Skip to content
D
Documentation

Selectors and subscriptions

concept
3 min readUpdated

A selector picks the state a component needs; a subscription connects that component, or another listener, to future store updates.

Your store is a hook: create it with state and actions, then select what each component needs. A snapshot read answers “what is the state now?” without subscribing. A hook subscription keeps the selected value current, and selector-output equality determines whether a store update causes a React re-render.

How updates reach a component

create returns a hook with store methods attached. Calling its getState() method reads the current state without registering a listener. Calling the hook in a component subscribes through React and returns the selector's output.

mermaid
flowchart TD
  A["Action calls set()"] --> B["Store updates state and notifies listeners"]
  B --> C["React reads the selector output"]
  C --> D["Compare output with Object.is"]
  D --> E["Different: re-render with the new output"]
  D --> F["Equal: no store-driven re-render"]
  B --> G["Explicit subscribe() listener runs"]
  H["getState()"] --> I["Current snapshot, no subscription"]

React compares computed selector outputs with Object.is. Selecting a number or an existing action function works without constructing a wrapper. Calling the hook without a selector returns the entire state, so every change to that state can cause a re-render.

Compare a snapshot with a subscription

Use this as src/App.tsx in your React application. It follows the starter's separate selectors for state and actions. The screen shows a live count, a count captured at mount, and an increment button.

tsx
import { useEffect, useState } from 'react'
import { create } from 'zustand'

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

const useCounterStore = create<CounterStore>()((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

export default function App() {
  const count = useCounterStore((state) => state.count)
  const inc = useCounterStore((state) => state.inc)
  const [countAtMount] = useState(() => useCounterStore.getState().count)

  useEffect(() => {
    const unsubscribe = useCounterStore.subscribe((state, previousState) => {
      console.log('Count changed:', previousState.count, '→', state.count)
    })
    return unsubscribe
  }, [])

  return (
    <main>
      <p>Live count: {count}</p>
      <p>Count at mount: {countAtMount}</p>
      <button onClick={inc}>Increment</button>
    </main>
  )
}

Click Increment. The live count changes; the count captured at mount stays unchanged. Each later getState() call reads the latest state, but the number already captured does not update itself.

The explicit subscribe() listener runs synchronously when the store changes and receives both the new and previous full state. It returns an unsubscribe function, which the effect uses as cleanup on unmount. Registering this listener does not itself request a React render; the count selector provides that connection in this sample.

Keep computed outputs stable

useShallow returns a selector that reuses its previous output reference when the new output is shallowly equal. This lets you construct an object or array from several state picks without treating an equivalent wrapper as a new value.

Install React explicitly if your project does not already include it; it is an optional peer dependency of Zustand:

bash
npm install zustand react react-dom

The bear-meals example illustrates the distinction: changing a meal changes the store, but not the list of bear names. Replace src/App.tsx with this complete example. It renders the names, papa bear's meal, and a button to change that meal.

Here, createStore creates a vanilla store and useStore connects its selected values to React.

tsx
import { createStore, useStore } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

type BearMeals = {
  papaBear: string
  mamaBear: string
  littleBear: string
}

const mealsStore = createStore<BearMeals>()(() => ({
  papaBear: 'large porridge-pot',
  mamaBear: 'middle-size porridge pot',
  littleBear: 'A little, small, wee pot',
}))

function BearNames() {
  const names = useStore(mealsStore, useShallow((state) => Object.keys(state)))
  return <p>Bear names: {names.join(', ')}</p>
}

function MealControls() {
  const meal = useStore(mealsStore, (state) => state.papaBear)

  return (
    <section>
      <p>Papa bear's meal: {meal}</p>
      <button
        onClick={() => mealsStore.setState({ papaBear: 'a large pizza' })}
      >
        Order pizza
      </button>
    </section>
  )
}

export default function App() {
  return (
    <main>
      <BearNames />
      <MealControls />
    </main>
  )
}

Click Order pizza. The meal text changes to a large pizza; the names remain papaBear, mamaBear, littleBear. Although Object.keys() constructs a new array, its entries are unchanged. useShallow returns the previous array reference, so this meal update does not cause a store-driven re-render of BearNames.

Shallow comparison checks top-level values, not nested contents recursively. It does not stop re-renders caused by a component's parent or its own React state.

Avoid fresh references in v5 selectors

In v5, a selector that returns a fresh object, array, or fallback function on every read can cause an infinite update loop. Keep the output reference stable: use separate selectors for individual values, wrap shallowly equal computed outputs in useShallow, or declare a fallback function outside the component rather than creating it inside the selector. This applies both to bound hooks from create and to useStore.

For custom equality functions and further rendering techniques, see Prevent unnecessary rerenders.

The API involved

  • getState() returns the current state without subscribing. Use it in event handlers or other imperative code when you need a fresh snapshot.
  • The hook returned by create selects and subscribes in React without a provider. With a vanilla store from createStore, use useStore(store, selector) for that connection instead.
  • subscribe(listener) listens to full-state changes and returns an unsubscribe function. It does not call the listener at registration.
  • subscribeWithSelector adds subscribe(selector, listener, options) for imperative listeners interested in a slice. The listener receives the new and previous selected values. Its equalityFn defaults to Object.is; fireImmediately optionally calls the listener at registration with the current selected value as both arguments. See Subscribe outside React for setup and cleanup.
  • useShallow(selector) preserves a shallowly equal output reference so React's output comparison can recognize it as unchanged.

Try the live Zustand demo to see a store-backed counter, or continue with State and actions for updating the state your selectors read.

Was this page helpful?