Optimizing Nested State Selectors
To prevent unnecessary re-renders when selecting nested state in Zustand (v4.x/v5.x), you must ensure the selector returns a referentially stable value. Because the default equality check is Object.is, any selector that returns a new object reference will trigger a re-render, even if the underlying data is identical.
Direct Answers to Selector Behavior
- Does a selector returning a nested object trigger a re-render on every store update? No, not on every update, but it will trigger a re-render whenever the selector returns a new reference. If your selector returns a stable reference to a subtree that hasn't changed, it won't re-render. However, if the selector creates an inline object (e.g.,
state => ({ value: state.nested.value })), it will trigger a re-render on every single store change.
- Can
zustand/shallow detect changes inside nested properties? No. shallow only performs a first-level comparison of the properties of the object returned by the selector. It does not recursively check nested objects. If a property within that nested object changes but the nested object's reference remains the same (mutation), it won't detect it; if the nested object's reference changes, it will trigger a re-render regardless of whether the leaf values are the same.
- What is the pattern for stable selection? The most reliable pattern is to select the primitive leaf value directly.
Implementation Patterns
1. Leaf-Level Selection (Recommended)
Instead of selecting a parent object, create multiple selectors or a single selector for the specific primitive value needed. This ensures Object.is only triggers a re-render when the actual value changes.
// Avoid this
const user = useStore(state => state.user);
// Do this
const userName = useStore(state => state.user.name);
const userEmail = useStore(state => state.user.email);
2. Using Shallow Equality for Flat Objects
If you must return an object, use shallow to prevent re-renders when the selected properties haven't changed. Note that this only works if the properties being compared are primitives or stable references.
import { shallow } from 'zustand/shallow';
const { name, email } = useStore(
(state) => ({ name: state.user.name, email: state.user.email }),
shallow
);
Assumptions and Verification
This guidance assumes you are using immutable state updates (e.g., using the spread operator ...state). If you are using immer, the referential stability of unchanged sub-trees is handled automatically.
Verification Step: To verify your selectors are working, wrap your component in a console.log or use the React DevTools "Highlight updates when components render" feature. Update a piece of state that is not used by the component; if the component re-renders, your selector is returning a new reference.
Diagnostic Detail: Are you using immer for state updates? If so, the referential stability of nested objects is preserved, making shallow less critical for existing sub-trees but still necessary for inline object literals.