Skip to content
D
Documentation

Optimize computed selections

how-to
2 min readUpdated

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:

bash
npm install zustand react

Select individual values

Create the store with create, then select each primitive or action separately:

tsx
import { 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 mounted search field is visible.

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:

tsx
import { 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>
}
The mounted component displays the three bear names.

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:

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

tsx
import { 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>
}
The mounted comparison result displays true.

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:

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

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

Avoid unstable selector loops

In v5, this selector creates a new array on every render:

tsx
type 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?