Skip to content
D
Documentation

Prevent unnecessary rerenders

how-to
4 min readUpdated

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:

bash
npm install zustand react react-dom
tsx
import { 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.

tsx
import { 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 bear names appear above papa bear's initial meal and the Order pizza button.

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:

bash
npm 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.

tsx
import { 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.

tsx
import { 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.

ArgumentTypeDefaultWhat it does
useShallow selector(state: S) => URequiredComputes the output whose previous reference is reused when shallowly equal.
createWithEqualityFn default equality<U>(a: U, b: U) => booleanundefinedSupplies the comparison for bound-hook calls that do not pass their own equality function.
Bound hook equality(a: U, b: U) => booleanStore's default equalityOverrides the comparison for that subscription.
useStoreWithEqualityFn equality(a: U, b: U) => booleanundefinedSupplies 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.

See a bound store and counter running in the Zustand live demo.

Was this page helpful?

Prevent unnecessary rerenders — zustand · GPT-6.1 Sol