Limit React re-renders with Zustand store selectors
Use Zustand's useStore selector so components re-render only when the state slice they read actually changes.
29 Mar 2026, 09:31 UTC

The problem: subscribing to the whole store causes extra renders
When a React component subscribes to a Zustand store without a selector, it re-renders whenever any state in that store changes—even state the component never displays. In a busy store, unrelated updates such as a status toggle or a background counter force UI components to reconcile unnecessarily.
Useful takeaway
Pass a selector function to useStore. The component re-renders only when the value returned by that selector changes. Other store updates are ignored by that component.
How useStore selectors work
create builds a Zustand store. useStore(store, selector) is the underlying React hook that subscribes a component to that store. The selector receives the full state tree and returns the slice the component needs. Zustand compares the returned slice against the previous result; if it is unchanged, the component skips the render.
Zustand does not deep-compare selector results by default. Returning a new object or array from the selector counts as a change, even if the values inside are identical.
Worked store configuration
This example assumes a React project with Zustand installed. Run npm install zustand in the project root. No special permissions are required.
import { create, useStore } from 'zustand';
const store = create((set) => ({
count: 0,
status: 'idle',
increment: () => set((state) => ({ count: state.count + 1 })),
setStatus: (status) => set({ status }),
}));
function CountDisplay() {
const count = useStore(store, (state) => state.count);
return Count: {count};
}
function StatusDisplay() {
const status = useStore(store, (state) => state.status);
return Status: {status};
}
function Controls() {
const increment = useStore(store, (state) => state.increment);
const setStatus = useStore(store, (state) => state.setStatus);
return (
Increment count
setStatus('ready')}>Set status ready
);
}
Clicking Increment count updates count. CountDisplay re-renders; StatusDisplay does not. Clicking Set status ready updates status; StatusDisplay re-renders; CountDisplay does not.
In everyday code you usually use the hook returned by create instead of passing the store instance directly: const count = useAppStore((state) => state.count). useStore is the lower-level hook that makes that binding possible.
Selector identity and stability
Inline object selectors create a new reference on every call and can defeat the purpose of selecting a slice.
// Risky: returns a new object every time the selector runs
const slice = useStore(store, (state) => ({
count: state.count,
status: state.status,
}));
Because Zustand does not deep-compare, that new object triggers a re-render even when count and status have not changed. Prefer primitive selectors:
const count = useStore(store, (state) => state.count);
const status = useStore(store, (state) => state.status);
If you need several values, call useStore multiple times or derive additional values in the component from primitives you have already selected. Keep selectors pure: do not call setState or trigger renders inside a selector, or you can create render loops.
Reading state outside React
Zustand also exposes the store directly for non-React code.
const current = store.getState();
const unsubscribe = store.subscribe((state) => {
console.log('state changed:', state);
});
// Later
unsubscribe();
This is useful for synchronizing state with logging, analytics, or legacy code that lives outside the React tree.
Limits and common mistakes
- No deep comparison by default. Returning a new array or object from a selector marks it as changed.
- Unstable selector references. Defining the selector inline inside a component creates a new function reference on every render. The selector still runs, but the returned value determines whether a render happens.
- Side effects in selectors. Reading state is safe; mutating state or scheduling updates inside a selector is not.
- Version differences. Selector equality and middleware behavior can differ between Zustand releases. Verify your installed version before relying on subtle edge cases.
Practical verification
- Open a terminal in the project root and run
npm install zustand. - Add two components to a page: one that selects the full store state and one that selects a primitive slice.
- Trigger an update to unrelated state and confirm only the slice-based component re-renders.
- Use React DevTools Profiler or a temporary render-count wrapper to measure renders before and after adding the selector. Remove the wrapper before shipping; it adds overhead.
- Check the installed version with
npm list zustandorcat node_modules/zustand/package.json, then compare the selector API against that version's release notes.
Risk: adding render counters or Profiler traces changes timing characteristics slightly. Use them for diagnosis, not production monitoring.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.