Redux and Zustand solve the same problem — centralized state — but they distribute re-renders in completely different ways. Redux, without selector discipline, tends to re-render more broadly by default. Zustand and Jotai were built specifically to avoid that by scoping subscriptions down to the atom or slice level. Confusing “global state” with “one re-render pattern” is where most of the performance problems in this post start.

None of these libraries are inherently slow. What causes trouble is using each one as if it were the other — treating a Zustand store like a Redux store, or an Jotai atom like a plain object you mutate freely. Below is a checklist organized by symptom, so you can jump to whatever’s actually happening in your app rather than reading a general theory of state management.


Symptom: Every Component Re-renders When Any Slice of State Changes

Cause: This is almost always a selector problem, not a library problem. If a component reads the entire store object instead of a narrow slice, it will re-render on every single state update, regardless of whether the data it cares about changed.

In Redux, this looks like:

// Re-renders on every store change, even unrelated ones
const state = useSelector((state) => state);

In Zustand, the equivalent mistake is subscribing to the whole store instead of a specific field:

// Subscribes to everything — defeats Zustand's purpose
const store = useStore();

Fix: Select only the specific piece of state a component actually needs. For Redux, that means narrow selectors, ideally memoized with reselect if any derivation is involved. For Zustand, pass a selector function directly into the hook.

// Redux — narrow, memoized selector
const username = useSelector((state) => state.user.name);

// Zustand — subscribe to one field only
const username = useStore((state) => state.username);

The difference isn’t cosmetic. A component subscribed to state.user.name only re-renders when that specific value changes, not when an unrelated slice like state.cart updates.


Symptom: Redux App Feels Sluggish Despite “Optimized” Selectors

Cause: Selectors returning new object or array references on every call will defeat useSelector’s equality check, even if the underlying data hasn’t changed. This is the Redux equivalent of the inline-object problem that plagues React.memo.

// New array reference every render — triggers re-render every time
const activeTodos = useSelector((state) =>
  state.todos.filter((t) => !t.completed)
);

Fix: Memoize the derived selector with reselect, so the same reference is returned when the inputs haven’t changed.

import { createSelector } from 'reselect';

const selectTodos = (state) => state.todos;

const selectActiveTodos = createSelector(
  [selectTodos],
  (todos) => todos.filter((t) => !t.completed)
);

Confirm this with the React DevTools Profiler rather than assuming the memoized selector fixed things. A missing or overly broad dependency in the selector’s inputs can quietly recompute — and return a new reference — on every dispatch.


Symptom: Zustand Component Updates Even Though It Only Reads One Field

Cause: This usually traces back to how the selector is written, not the store itself. Destructuring multiple fields directly from the hook call returns a new object literal every render, which fails Zustand’s default reference equality check.

// Returns a new object every call — re-renders regardless of what changed
const { username, avatar } = useStore((state) => ({
  username: state.username,
  avatar: state.avatar,
}));

Fix: Either split this into two separate selector calls, or supply a custom equality function such as shallow from Zustand’s middleware.

import { shallow } from 'zustand/shallow';

const { username, avatar } = useStore(
  (state) => ({ username: state.username, avatar: state.avatar }),
  shallow
);

For most cases, two single-field selector calls are simpler to reason about and avoid the equality-function question entirely.


Symptom: Jotai Atoms Cause Re-renders in Unrelated Components

Cause: Jotai’s model is atom-based rather than slice-based, so the usual mistake looks different. It happens when several pieces of state that update independently are combined into one large atom instead of being split apart.

// One large atom means any field change re-renders every consumer
const userAtom = atom({ name: '', email: '', preferences: {} });

Any component reading userAtom, even for just the name field, re-renders whenever preferences changes too, since they’re bundled into a single atom.

Fix: Split state into smaller, independent atoms so components only subscribe to what they actually use.

const nameAtom = atom('');
const emailAtom = atom('');
const preferencesAtom = atom({});

If related atoms need to be read together in a few places, a derived read-only atom can compose them without forcing every consumer back onto one bundled object.


Symptom: State Updates Feel Instant Locally but Slow Down at Scale

Cause: This one is less about the library and more about update frequency versus subscriber count. A store updated dozens of times per second (scroll position, mouse coordinates, live socket data) will strain any subscriber that re-renders on each tick, no matter how well-scoped the selectors are.

Fix: Decouple high-frequency updates from the parts of the tree that don’t need to render on every tick. Options that consistently help:

  • Throttle or debounce the state updates themselves before they hit the store.
  • Move the frequently-changing value into local component state instead of global state, if nothing else in the app depends on it.
  • In Zustand, use subscribe outside of React to react to changes imperatively (e.g., updating a canvas or a ref) instead of routing every tick through a re-render.
// Imperative subscription — no re-render triggered
useEffect(() => {
  const unsub = useStore.subscribe(
    (state) => state.scrollY,
    (scrollY) => {
      canvasRef.current.style.transform = `translateY(${scrollY}px)`;
    }
  );
  return unsub;
}, []);

Symptom: Redux DevTools Show Dozens of Dispatches Per Second

Cause: Often this is a sign that state is being updated at a granularity that doesn’t match how it’s consumed — for instance, dispatching an action on every keystroke of a form field that’s stored globally when only one component ever reads it.

Fix: Ask whether this state needs to be global at all. Form input values, hover states, and other transient UI state are frequently better off as local useState, reserving the global store for state that’s genuinely shared across distant parts of the tree. Moving state out of a global store isn’t a downgrade — it’s matching the tool to the actual sharing requirement.


Quick Reference: Symptom-to-Fix Table

Symptom Likely Cause Fix
Every component re-renders on any state change Selecting the whole store instead of a slice Narrow selectors, per-field subscriptions
Redux re-renders despite selectors Selector returns new reference each call Memoize with reselect
Zustand re-renders on unrelated field Object literal returned from selector Split selectors or use shallow
Jotai atom over-triggers renders State bundled into one large atom Split into smaller independent atoms
Performance degrades under high-frequency updates Too many components re-rendering per tick Throttle updates or subscribe imperatively
Excessive dispatches in DevTools State is global but only locally relevant Move state back to local useState

Choosing Between the Three Isn’t the Real Question

Redux, Zustand, and Jotai can all be made fast, and all three can be made slow by the same underlying mistake: subscribing too broadly to state that changes too often. The library choice affects the defaults and the ergonomics of getting this right, but it doesn’t remove the need to think about what each component actually reads versus what causes it to re-render.

Which of these symptoms matches what you’re seeing in your app? Share your store setup and the component that’s misbehaving, and it’s usually possible to pinpoint whether the fix is a selector, a split atom, or a piece of state that never needed to be global in the first place.