Use a selector for the value your component needs, and wrap a selector that creates a new object or array with useShallow when its top-level values can stay the same.
When to use it
Use atomic selectors when you need individual values. Use useShallow when one component needs several values or a computed collection such as Object.keys(state). Zustand compares selector results with Object.is by default, so a newly created object or array is different even when its contents are unchanged.
Install Zustand and React before using either React entry point:
bashnpm install zustand react
Select individual values
Create the store with create, then select each primitive or action separately:
tsximport { create } from 'zustand'
type SearchStore = {
searchValue: string
setSearchValue: (value: string) => void
}
const useSearchStore = create<SearchStore>((set) => ({
searchValue: '',
setSearchValue: (value) => set({ searchValue: value }),
}))
export function SearchBox() {
const searchValue = useSearchStore((state) => state.searchValue)
const setSearchValue = useSearchStore((state) => state.setSearchValue)
return (
<label>
Search
<input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
/>
</label>
)
}
The component receives the current string and the action without constructing a wrapper object. It re-renders when either selected value changes.
Keep a computed selection stable
When the selector constructs an object or array, pass it through useShallow:
tsximport { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
type MealStore = {
papaBear: string
mamaBear: string
babyBear: string
}
const useMealStore = create<MealStore>(() => ({
papaBear: 'large porridge-pot',
mamaBear: 'middle-size porridge pot',
babyBear: 'A little, small, wee pot',
}))
export function BearNames() {
const names = useMealStore(
useShallow((state) => Object.keys(state)),
)
return <div style={{ height: '400px' }}>{names.join(', ')}</div>
}
useShallow returns a memoized selector. The component displays papaBear, mamaBear, babyBear, and changing a meal value does not change that array's top-level contents, so it does not cause an unnecessary render of BearNames. Adding or removing a store key changes the displayed names and causes the component to update.
The same pattern applies to several selected values. This complete component returns one stable object:
tsximport { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
type SearchStore = {
searchValue: string
setSearchValue: (value: string) => void
}
const useSearchStore = create<SearchStore>((set) => ({
searchValue: 'porridge',
setSearchValue: (value) => set({ searchValue: value }),
}))
export function SearchSummary() {
const selection = useSearchStore(
useShallow((state) => ({
searchValue: state.searchValue,
setSearchValue: state.setSearchValue,
})),
)
return (
<div style={{ height: '400px' }}>
<output>{selection.searchValue}</output>
</div>
)
}
The returned object keeps its previous reference when its selected properties are shallow-equal. If you use an array, object, Map, or Set, the comparison is shallow: nested values are compared by reference rather than recursively.
Compare values explicitly
Use shallow when you need a boolean comparison outside a store subscription:
tsximport { shallow } from 'zustand/shallow'
const previous = { firstName: 'John', lastName: 'Doe' }
const next = { firstName: 'John', lastName: 'Doe' }
const unchanged = shallow(previous, next)
export function ComparisonResult() {
return <output>{String(unchanged)}</output>
}
unchanged is true because the top-level properties match. A nested object with a different reference makes the comparison false even when its nested contents look identical.
Use a custom equality function when shallow comparison is not enough
For a store that needs a custom equality function, use createWithEqualityFn from zustand/traditional:
tsximport { createWithEqualityFn } from 'zustand/traditional'
type ResultStore = {
result: { value: number; label: string }
}
const useResultStore = createWithEqualityFn<ResultStore>(() => ({
result: { value: 1, label: 'one' },
}))
const sameResult = (left: ResultStore['result'], right: ResultStore['result']) =>
left.value === right.value && left.label === right.label
export function Result() {
const result = useResultStore((state) => state.result, sameResult)
return <output>{result.label}: {result.value}</output>
}
The component uses sameResult for this selection, so it updates only when that comparison returns false. The zustand/traditional entry point requires the use-sync-external-store peer dependency; install it with the package:
bashnpm install zustand react use-sync-external-store
Avoid unstable selector loops
In v5, this selector creates a new array on every render:
tsxtype SearchStore = {
searchValue: string
setSearchValue: (value: string) => void
}
const unstableSelector = (state: SearchStore) => [
state.searchValue,
state.setSearchValue,
]
Because the default comparison is Object.is, the new reference can trigger a maximum-update-depth loop. Replace it with the useShallow version above, or use two atomic selectors. Do not return a fallback function created inside a selector; define the fallback once so the selector returns a stable reference.
Was this page helpful?