Zustand Selectors: Render Only the State Your Component Needs
Zustand components re-render on every store change unless you use selectors. Learn how to subscribe to specific state slices and avoid referential-equality traps.
08 Jan 2026, 14:34 UTC

The Problem: One Store Change, Too Many Re-renders
You build a Zustand store with a theme setting, user preferences, and a long form draft. A badge component only shows the theme, yet it re-renders every time a user types in the form. By default, useStore without a selector subscribes to the entire store, so any state update triggers a render in every component using that hook.
Takeaway: Pass a selector function to useStore so your component re-renders only when the slice it actually displays changes.
How Zustand Selectors Work
A selector receives the full store state and returns the value your component needs. Zustand compares the returned value with the previous result using Object.is. If the reference is unchanged, React skips the render.
const useTheme = useAppStore((state) => state.theme)
Because state.theme is a string, Object.is("dark", "dark") is true and the component does not re-render when unrelated state changes.
Worked Example: Theme Badge and Form Input
Define a store with unrelated state:
import { create } from 'zustand'
const useAppStore = create((set) => ({
theme: 'light',
setTheme: (next) => set({ theme: next }),
draft: { title: '', body: '' },
setDraft: (field, value) =>
set((state) => ({ draft: { ...state.draft, [field]: value } })),
}))
The theme badge subscribes only to theme:
function ThemeBadge() {
const theme = useAppStore((state) => state.theme)
return {theme}
}
The title input subscribes only to its field:
function TitleInput() {
const title = useAppStore((state) => state.draft.title)
const setDraft = useAppStore((state) => state.setDraft)
return (
setDraft('title', event.target.value)}
/>
)
}
Verification: Open React DevTools Profiler, toggle the theme, and confirm TitleInput does not commit a render. Then type in the title field and confirm ThemeBadge does not render.
The Referential-Equality Trap
Selectors compare references, not deep values. Returning a new object or array inside the selector creates a fresh reference on every call, which defeats the optimization.
// Bad: new object on every call, re-renders on any store change
const user = useAppStore((state) => ({
name: state.user.name,
email: state.user.email,
}))
Select individual primitives when possible. If you need an object slice, use Zustand's shallow helper:
import { shallow } from 'zustand/shallow'
const user = useAppStore((state) => state.user, shallow)
shallow performs a one-level comparison of the returned object's own enumerable properties. Nested objects still compare by reference.
Limitation: Selectors Do Not Stop Store Notifications
A selector prevents the component from re-rendering, but the store still notifies every subscriber. With dozens of fine-grained selectors on one store, notification overhead can add up. For high-frequency values such as mouse coordinates, keep them in a ref or a separate lightweight store instead of putting everything in one Zustand store.
Actionable: Add Selectors Before Broader Optimizations
Start with plain useStore() for simplicity. When a component re-renders too often, add a selector for the exact state it displays. Use shallow only when you need an object slice and cannot split it into primitive selectors. Check React DevTools Profiler after each change to confirm render commits drop.
- Identify components that re-render on unrelated store updates.
- Replace
useStore()withuseStore((state) => state.field). - If selecting an object, test with
shallowfromzustand/shallow. - Profile with React DevTools to verify render counts drop.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.