Skip to content
D
Documentation

Selectors and rendering

concept
2 min readUpdated

Selectors decide which part of a store a component reads; the selected result and its equality function decide whether that component renders again.

How the pieces fit

mermaid
flowchart LR
  Store["Zustand store"] --> Selector["selector(state)"]
  Selector --> Result["selected result"]
  Result --> Equality["Object.is or equality function"]
  Equality -->|different| Render["component renders"]
  Equality -->|equal| Skip["component keeps its render"]

The default comparison is Object.is. Selecting an atomic value such as a number, string, or action usually gives a stable result. A selector that constructs an object or array creates a new reference, even when its contents are unchanged, so use a stable selector output or a shallow comparison for that case.

Select atomic values directly

Create the store with create, then pass a selector to the returned hook. Each selector subscribes the component to one result, so changing another state property does not change that result.

tsx
import { create } from 'zustand'

type MealStore = {
  papaBear: string
  count: number
  setMeal: (meal: string) => void
}

const useMealStore = create<MealStore>()((set) => ({
  papaBear: 'large porridge-pot',
  count: 0,
  setMeal: (meal) => set({ papaBear: meal }),
}))

export default function App() {
  const meal = useMealStore((state) => state.papaBear)
  const setMeal = useMealStore((state) => state.setMeal)

  return (
    <main>
      <p>Meal: {meal}</p>
      <button type="button" onClick={() => setMeal('a large pizza')}>
        Order pizza
      </button>
    </main>
  )
}
The mounted counter displays the selected meal and an Order pizza button.

The component reads the meal and action separately. Clicking the button changes the displayed meal; the count state is not part of either selected result.

Select several values with useShallow

Wrap a computed selector with useShallow when it returns an object or array whose entries are shallow-equal to the previous result.

tsx
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

type FruitStore = {
  apples: number
  oranges: number
  addApple: () => void
}

const useFruitStore = create<FruitStore>()((set) => ({
  apples: 1,
  oranges: 2,
  addApple: () => set((state) => ({ apples: state.apples + 1 })),
}))

export default function App() {
  const fruit = useFruitStore(
    useShallow((state) => ({ apples: state.apples, oranges: state.oranges })),
  )
  const addApple = useFruitStore((state) => state.addApple)

  return (
    <main>
      <p>
        Apples: {fruit.apples}; oranges: {fruit.oranges}
      </p>
      <button type="button" onClick={addApple}>
        Add apple
      </button>
    </main>
  )
}
The mounted fruit component displays apple and orange counts with an Add apple button.

For a direct comparison, shallow takes two values and returns a boolean. It compares object entries and ordered iterable values using Object.is for their entries; use it when you need to supply an equality function yourself.

Use a custom equality function

In v5, create does not accept a custom equality function. Use createWithEqualityFn from zustand/traditional when a store needs a default equality function. The zustand/traditional entry point requires the use-sync-external-store package.

tsx
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'

type PositionStore = {
  x: number
  y: number
  move: (x: number, y: number) => void
}

const usePositionStore = createWithEqualityFn<PositionStore>()(
  (set) => ({
    x: 0,
    y: 0,
    move: (x, y) => set({ x, y }),
  }),
  shallow,
)

export default function App() {
  const position = usePositionStore((state) => ({ x: state.x, y: state.y }))
  const move = usePositionStore((state) => state.move)

  return (
    <main>
      <p>
        Position: {position.x}, {position.y}
      </p>
      <button type="button" onClick={() => move(10, 20)}>
        Move
      </button>
    </main>
  )
}
The mounted position component displays coordinates and a Move button.

The position object is compared shallowly, so the component re-renders when x or y changes and does not re-render for an update that leaves both selected values equal. Install the peer package before using this entry point:

bash
npm install zustand react use-sync-external-store

When the store is vanilla and the equality choice belongs to a particular component, use useStoreWithEqualityFn with the store API, a selector, and that component's equality function.

tsx
import { createStore } from 'zustand'
import { shallow } from 'zustand/shallow'
import { useStoreWithEqualityFn } from 'zustand/traditional'

type PositionStore = {
  x: number
  y: number
  move: (x: number, y: number) => void
}

const positionStore = createStore<PositionStore>()((set) => ({
  x: 0,
  y: 0,
  move: (x, y) => set({ x, y }),
}))

export default function App() {
  const position = useStoreWithEqualityFn(
    positionStore,
    (state) => ({ x: state.x, y: state.y }),
    shallow,
  )
  const move = useStoreWithEqualityFn(positionStore, (state) => state.move, shallow)

  return (
    <main>
      <p>
        Position: {position.x}, {position.y}
      </p>
      <button type="button" onClick={() => move(10, 20)}>
        Move
      </button>
    </main>
  )
}
The mounted vanilla-store component displays coordinates and a Move button.

Install React explicitly when using either React entry point; npm install zustand does not add this optional peer dependency.

bash
npm install zustand react

Bind a vanilla store when the store itself is separate

For a store created outside React, createStore returns a store API. Pass that API to useStore and give the hook a selector. This keeps the same selector-and-equality rule while allowing the store instance to be created and supplied by another part of the application.

tsx
import { createStore } from 'zustand'
import { useStore } from 'zustand'

type CounterStore = {
  count: number
  increment: () => void
}

const counterStore = createStore<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

export default function App() {
  const count = useStore(counterStore, (state) => state.count)
  const increment = useStore(counterStore, (state) => state.increment)

  return (
    <main>
      <p>Count: {count}</p>
      <button type="button" onClick={increment}>
        Increment
      </button>
    </main>
  )
}

The component displays the counter from the separate store and updates when count changes.

Avoid unstable selector outputs

Do not return a fresh object or array from a create selector without handling its equality. In v5, the default Object.is comparison sees each fresh reference as different and the selector can cause an infinite update loop. Choose one of these forms instead:

  • Select each value separately.
  • Use createWithEqualityFn with an equality function when you need that store-wide behavior.

Use a stable fallback value when a selector returns a fallback function; creating a new fallback function in the selector has the same unstable-reference problem.

Was this page helpful?