Use selectors to subscribe to the values your component renders. Select individual values when possible; wrap computed selections with useShallow when shallow comparison is sufficient. Use the traditional hooks when you need a custom equality check.
Selector-output equality determines whether a store update causes a component to rerender. The default comparison is Object.is; useShallow returns a selector that reuses its previous output when the next output is shallowly equal.
1. Select individual values
Create a bound store with create, then select state and actions separately. This browser example renders a counter and a text input. Changing the text updates TextInput, but does not cause a store-driven rerender of Counter: neither its selected count nor its action changes.
Use each example below as your React project's src/main.tsx, with a <div id="root"></div> in its HTML entry point.
Install Zustand and its React peer, plus React DOM to mount the examples:
bashnpm install zustand react react-dom
tsximport { createRoot } from 'react-dom/client'
import { create } from 'zustand'
type CounterStore = {
count: number
text: string
inc: () => void
setText: (text: string) => void
}
const useCounterStore = create<CounterStore>()((set) => ({
count: 0,
text: 'hello',
inc: () => set((state) => ({ count: state.count + 1 })),
setText: (text) => set({ text }),
}))
function Counter() {
const count = useCounterStore((state) => state.count)
const inc = useCounterStore((state) => state.inc)
return <button onClick={inc}>Count: {count}</button>
}
function TextInput() {
const text = useCounterStore((state) => state.text)
const setText = useCounterStore((state) => state.setText)
return (
<label>
Text <input value={text} onChange={(event) => setText(event.target.value)} />
</label>
)
}
function App() {
return <main><Counter /><TextInput /></main>
}
createRoot(document.getElementById('root')!).render(<App />)
Clicking the counter increments the displayed count. Each hook returns its selected value, rather than the entire store.
2. Stabilize a computed selection
When you need a computed array or object, pass the selector through useShallow. It compares the output, not the whole store, and returns the previous output reference when the comparison succeeds.
For the store and selector pattern, see How Zustand works. Run this comparison in your React project's development mode: React's Profiler logs commits for a whole-store subscription and a shallow-selected names subscription. Click Order pizza: papa bear's displayed meal changes, and both lists remain papaBear, mamaBear, littleBear. Look in the browser console for an update from WholeStoreNames, but not from BearNames: the shallow-selected array's entries have not changed.
tsximport { Profiler } from 'react'
import { createRoot } from 'react-dom/client'
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
type Meals = {
papaBear: string
mamaBear: string
littleBear: string
}
const useMeals = create<Meals>()(() => ({
papaBear: 'large porridge-pot',
mamaBear: 'middle-size porridge pot',
littleBear: 'A little, small, wee pot',
}))
function BearNames() {
const names = useMeals(useShallow((state) => Object.keys(state)))
return <p>{names.join(', ')}</p>
}
function WholeStoreNames() {
const meals = useMeals()
return <p>{Object.keys(meals).join(', ')}</p>
}
function PapaMeal() {
const meal = useMeals((state) => state.papaBear)
return (
<section>
<p>Papa bear's meal: {meal}</p>
<button onClick={() => useMeals.setState({ papaBear: 'a large pizza' })}>
Order pizza
</button>
</section>
)
}
function App() {
return (
<main>
<Profiler id="WholeStoreNames" onRender={(id, phase) => console.log(id, phase)}>
<WholeStoreNames />
</Profiler>
<Profiler id="BearNames" onRender={(id, phase) => console.log(id, phase)}>
<BearNames />
</Profiler>
<PapaMeal />
</main>
)
}
createRoot(document.getElementById('root')!).render(<App />)
The same wrapper works for multiple state picks in an object or array. For example, select both a value and its action inside the wrapped selector. Shallow comparison checks top-level entries with Object.is; it does not recursively compare nested objects. A newly created nested object still compares differently.
3. Use custom equality when shallow comparison is not enough
In v5, create does not support a custom equality function. Use createWithEqualityFn from zustand/traditional for a bound hook with custom equality, or keep create and use useShallow for shallow comparison.
Install the traditional entry point's peer dependency in your project:
bashnpm install zustand react react-dom use-sync-external-store
Pass the shipped shallow comparator as the store's default equality function. You can override it for an individual subscription by passing a second argument to the bound hook.
This example displays the number of completed groups of ten. Each click increments the store's count, but GroupsOfTen keeps its selected output while both counts belong to the same group. Its displayed value changes from zero to one on the tenth click.
tsximport { createRoot } from 'react-dom/client'
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'
type CounterStore = {
count: number
inc: () => void
}
const useCounterStore = createWithEqualityFn<CounterStore>()(
(set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}),
shallow,
)
function sameGroup(previous: number, next: number) {
return Math.floor(previous / 10) === Math.floor(next / 10)
}
function GroupsOfTen() {
const count = useCounterStore((state) => state.count, sameGroup)
return <p>Completed groups of ten: {Math.floor(count / 10)}</p>
}
function IncrementButton() {
const inc = useCounterStore((state) => state.inc)
return <button onClick={inc}>Add one</button>
}
function App() {
return <main><GroupsOfTen /><IncrementButton /></main>
}
createRoot(document.getElementById('root')!).render(<App />)
Only ignore differences your component does not render. Here, the component renders the group number, not the raw count.
For an existing vanilla store created with createStore, use useStoreWithEqualityFn. Pass the store instance, a selector, and an optional equality function. It returns the selected data and applies that equality check to the subscription; you do not need to recreate the store as a bound hook.
Use useStore for subscriptions that do not need custom equality. This mounted example uses it to select the increment action and uses useStoreWithEqualityFn to select the count with the group comparison. Click Add one ten times to change the displayed group from zero to one.
tsximport { createRoot } from 'react-dom/client'
import { createStore, useStore } from 'zustand'
import { useStoreWithEqualityFn } from 'zustand/traditional'
type CounterStore = {
count: number
inc: () => void
}
const counterStore = createStore<CounterStore>()((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function sameGroup(previous: number, next: number) {
return Math.floor(previous / 10) === Math.floor(next / 10)
}
function GroupsOfTen() {
const count = useStoreWithEqualityFn(
counterStore,
(state) => state.count,
sameGroup,
)
return <p>Completed groups of ten: {Math.floor(count / 10)}</p>
}
function IncrementButton() {
const inc = useStore(counterStore, (state) => state.inc)
return <button onClick={inc}>Add one</button>
}
function App() {
return <main><GroupsOfTen /><IncrementButton /></main>
}
createRoot(document.getElementById('root')!).render(<App />)
Comparison parameters
These APIs take selector and comparison arguments rather than an options object.
| Argument | Type | Default | What it does |
|---|---|---|---|
useShallow selector | (state: S) => U | Required | Computes the output whose previous reference is reused when shallowly equal. |
createWithEqualityFn default equality | <U>(a: U, b: U) => boolean | undefined | Supplies the comparison for bound-hook calls that do not pass their own equality function. |
| Bound hook equality | (a: U, b: U) => boolean | Store's default equality | Overrides the comparison for that subscription. |
useStoreWithEqualityFn equality | (a: U, b: U) => boolean | undefined | Supplies the comparison for the selected output from a vanilla store. |
Pitfalls and verification
- Shallow equality is not deep equality. Select the primitive fields you render when nested object references change but those fields do not.
- These comparisons prevent store-driven rerenders, not renders caused by a parent or local React state. Use React DevTools' Profiler to inspect which components commit when you click the examples' buttons.
- For stable selector outputs in v5, see Selectors and subscriptions.
Live demo and related guides
See a bound store and counter running in the Zustand live demo.
- React quick start — set up your first store and React component.
- Selectors and subscriptions — define what each component subscribes to.
- State and actions — preserve references correctly when updating state.
Was this page helpful?