How to Prevent Unnecessary Re-Renders When Selecting Multiple State Slices in Zustand
Combine multiple state selections into a single selector wrapped with useShallow or use createWithEqualityFn to replace strict equality checks with shallow comparison, reducing multiple subscriptions to one and eliminating redundant renders.
When you read separate state properties through multiple useStore calls in the same component, Zustand creates independent subscriptions for each slice that can fire separately. According to the pmndrs/zustand source code, the idiomatic solution to prevent unnecessary re-renders when selecting multiple state slices in Zustand is to aggregate selections into one selector and apply shallow equality semantics.
Why Multiple Hooks Cause Extra Re-Renders
By default, the useStore hook in src/react.ts subscribes to the store using React.useSyncExternalStore and compares selector outputs with strict Object.is equality. When you write:
const nuts = useBearStore(state => state.nuts) // subscription #1
const honey = useBearStore(state => state.honey) // subscription #2
Each call registers its own subscription. If a single update changes both values, React renders the component twice—once per subscription—because each hook independently detects a change. Even if only one value changes, the component re-renders, but with stale props for the unchanged slice.
The Solution: Single Selector with Shallow Equality
To reduce the subscription count to one, return an object or array containing all needed slices from a single selector. However, because the default equality check uses Object.is, a newly created object { nuts, honey } is always considered different from the previous one, triggering a render on every store update regardless of content.
Zustand provides two mechanisms to solve this: useShallow for per-selector optimization, and createWithEqualityFn for global default behavior.
Method 1: Using useShallow (Recommended)
The useShallow helper exported from zustand/react/shallow applies shallow equality (=== on each property) to the selector’s result. It memoizes the previous output in a ref and only returns a new reference when a shallow property differs, preventing React from seeing a change when object contents are stable.
Implementation detail: useShallow lives in src/react/shallow.ts and internally calls the shallow utility from src/vanilla/shallow.ts.
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
const useBearStore = create(() => ({
nuts: 0,
honey: 0,
treats: {},
}))
export const BearInfo = () => {
// One subscription, shallow-checked object
const { nuts, honey } = useBearStore(
useShallow(state => ({ nuts: state.nuts, honey: state.honey }))
)
return <div>{nuts} nuts, {honey} honey</div>
}
You can also return an array:
const [nuts, honey] = useBearStore(
useShallow(state => [state.nuts, state.honey])
)
With this pattern, the component re-renders only when nuts or honey actually change, not when unrelated fields like treats update.
Method 2: Using createWithEqualityFn for Global Defaults
If you prefer not to wrap every selector individually, create a store that uses shallow equality by default. The createWithEqualityFn factory in src/traditional.ts builds a store whose useStoreWithEqualityFn hook (backed by useSyncExternalStoreWithSelector) accepts a default equality function applied to all selectors unless overridden.
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
const useBearStore = createWithEqualityFn(
set => ({
nuts: 0,
honey: 0,
addNut: () => set(state => ({ nuts: state.nuts + 1 })),
}),
shallow // default equality for all selectors
)
export const BearInfo = () => {
const { nuts, honey } = useBearStore(state => ({
nuts: state.nuts,
honey: state.honey,
})) // No useShallow wrapper needed
return <div>{nuts} – {honey}</div>
}
The implementation in src/traditional.ts wires createWithEqualityFnImpl to useStoreWithEqualityFn, passing your equalityFn (here, shallow) into the subscription mechanism.
Method 3: Custom Equality Per Call
For one-off cases where you need shallow equality on a standard store without changing global defaults, import useStoreWithEqualityFn directly from zustand/traditional and provide the equality function as the third argument:
import { create } from 'zustand'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/vanilla/shallow'
const useBearStore = create(() => ({ nuts: 0, honey: 0 }))
export const Bear = () => {
const { nuts, honey } = useStoreWithEqualityFn(
useBearStore,
state => ({ nuts: state.nuts, honey: state.honey }),
shallow // override equality only for this call
)
return <div>{nuts} nuts, {honey} honey</div>
}
Summary
- Multiple
useStorecalls create multiple subscriptions, causing components to re-render once per changed slice even within the same update cycle. useShallowfromsrc/react/shallow.tswraps a selector to apply shallow equality, preventing new object references from triggering renders when property values are unchanged.createWithEqualityFnfromsrc/traditional.tsallows you to set shallow equality as the default for all selectors on a store, eliminating the need for repetitive wrapper code.- Single selectors combined with shallow comparison reduce the subscription count to one and ensure components only render when selected data actually changes.
Frequently Asked Questions
Why does selecting multiple slices cause extra renders?
Each call to useStore in src/react.ts invokes React.useSyncExternalStore to create a subscription tied to that specific selector. When state updates, every subscription checks its selector result independently using strict equality (Object.is). If you select nuts and honey separately, both subscriptions fire, causing React to schedule two renders for the same component. Combining selections into one selector reduces this to a single subscription and a single render check.
What is the difference between useShallow and createWithEqualityFn?
useShallow is a React hook that memoizes a single selector's output using shallow equality, ideal for standard stores created with create(). createWithEqualityFn is a store factory that builds a store where every selector uses a custom equality function (like shallow) by default. Use useShallow for opt-in optimization on specific selectors; use createWithEqualityFn when you want shallow comparison to be the default behavior across your entire store.
Can I use useShallow with array selectors?
Yes. useShallow works with any object or array returned from your selector. It performs shallow comparison on each element or property, so useShallow(state => [state.nuts, state.honey]) only returns a new array reference when nuts or honey change, preventing unnecessary re-renders just like object selectors.
Does shallow equality work for nested objects?
No. The shallow utility in src/vanilla/shallow.ts only checks one level deep using strict equality (===) on each property. If you select a nested object like { treats: state.treats } where treats is itself an object, shallow equality sees the object reference, not its contents. For deeply nested structures, you need to either flatten the selection, use a custom deep equality function (with performance trade-offs), or structure your store to avoid deep nesting in frequently selected slices.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →