Zustand React Patterns
Optimize Zustand re-renders with selectors, shallow comparison, useShallow, and transient subscriptions
When to Use
- Components re-render too often because they subscribe to the entire store
- Selecting multiple values from a store without causing extra re-renders
- Subscribing to state changes outside the React render cycle (animations, event handlers)
- Building performance-sensitive UIs with large stores
Instructions
- Always use a selector:
useStore((s) => s.field)instead ofuseStore(). This subscribes only tofield. - When selecting multiple fields, use
useShallowto prevent re-renders when the object shape is the same. - For derived values, compute them in the selector — avoid storing derived data.
- Use
useStore.subscribefor transient updates that should not trigger re-renders (animations, logging). - Use
useStoreWithEqualityFnfor custom equality comparisons beyond shallow.
// Selecting single values — optimal by default
function Counter() {
const count = useStore((s) => s.count); // Re-renders only when count changes
const increment = useStore((s) => s.increment); // Functions are stable references
return <button
}
// Selecting multiple values — use useShallow
import { useShallow } from 'zustand/react/shallow';
function UserProfile() {
// Without useShallow: re-renders on ANY store change because { name, email } is a new object every time
// With useShallow: re-renders only when name OR email change
const { name, email } = useStore(
useShallow((s) => ({ name: s.user.name, email: s.user.email }))
);
return <div>{name} ({email})</div>;
}
// Alternative: select as array
function UserProfile2() {
const [name, email] = useStore(
useShallow((s) => [s.user.name, s.user.email])
);
return <div>{name} ({email})</div>;
}
// Transient subscriptions — no re-renders
function AnimatedElement() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
// Subscribe to position changes without re-rendering
const unsub = useStore.subscribe(
(state) => state.position,
(position) => {
if (ref.current) {
ref.current.style.transform = `translateX(${position}px)`;
}
}
);
return unsub;
}, []);
return <div ref={ref}>Moving element</div>;
}
Details
Why selectors matter for performance: Zustand uses Object.is to compare the previous and next selector results. If the selector returns a new object every time ((s) => ({ a: s.a, b: s.b })), the comparison always fails and the component re-renders. useShallow uses shallow equality instead.
Selector stability patterns:
// Bad — new function reference every render
<List => useStore.getState().selectItem(id)} />
// Good — stable selector
const selectItem = useStore((s) => s.selectItem);
<List />
Auto-generating selectors: Create a helper that generates individual property selectors:
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
const createSelectors = <S extends StoreApi<object>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>;
store.use = {};
for (const k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s: any) => s[k]);
}
return store;
};
// Usage: const count = useStore.use.count();
subscribe with selector (Zustand v4+): The subscribe method accepts a selector and an equality function for targeted external subscriptions:
useStore.subscribe(
(state) => state.count, // selector
(count, prevCount) => {
// listener
console.log('Count changed:', prevCount, '->', count);
},
{ equalityFn: Object.is } // optional, default is Object.is
);
Source
https://zustand.docs.pmnd.rs/guides/prevent-rerenders-with-use-shallow
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.