//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use selector functions with useStore(selector) to subscribe to specific slices of state. Components only re-render when their selected value changes, preventing unnecessary renders.
"use client";
import { useCartStore } from "@/stores/cart-store";
function CartBadge() {
// Only re-renders when items array changes
const itemCount = useCartStore((state) => state.items.length);
return <span className="badge">{itemCount}</span>;
}
function CartTotal() {
// Only re-renders when total changes
const total = useCartStore((state) =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
return <span>${total.toFixed(2)}</span>;
}// stores/dashboard-store.ts
import { create } from "zustand";
interface DashboardStore {
user: { name: string; avatar: string };
notifications: { id: string; message: string }[];
theme: "light" | "dark";
sidebarOpen: boolean;
toggleSidebar: () => void;
toggleTheme: () => void;
addNotification: (message: string) => void;
}
export const useDashboardStore = create<DashboardStore>((set) => ({
user: { name: "Jane", avatar: "/avatar.png" },
notifications: [],
theme: "light",
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
addNotification: (message) =>
set((s) => ({
notifications: [...s.notifications, { id: crypto.randomUUID(), message }],
})),
}));// components/header.tsx
"use client";
import { useDashboardStore } from "@/stores/dashboard-store";
import { useShallow } from "zustand/react/shallow";
// Bad: re-renders on ANY store change
function HeaderBad() {
const store = useDashboardStore();
return <div>{store.user.name}</div>;
}
// Good: only re-renders when user.name changes
function HeaderGood() {
const userName = useDashboardStore((s) => s.user.name);
return <div>{userName}</div>;
}
// Good: multiple values with shallow comparison
function HeaderWithMultiple() {
const { name, avatar } = useDashboardStore(
useShallow((s) => ({ name: s.user.name, avatar: s.user.avatar }))
);
return (
<div>
<img src={avatar} alt={name} />
<span>{name}</span>
</div>
);
}
// Good: selecting multiple primitives with useShallow array
function NotificationBar() {
const [notifications, theme] = useDashboardStore(
useShallow((s) => [s.notifications, s.theme])
);
return (
<div className={theme}>
{notifications.length} notifications
</div>
);
}===) by default to compare the previous and next selected value.{} !== {}.useShallow from zustand/react/shallow performs a shallow comparison on the selected object or array, preventing re-renders when individual properties have not changed.Auto-generating selectors:
import { create } from "zustand";
import { StoreApi, UseBoundStore } from "zustand";
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
function createSelectors<S extends UseBoundStore<StoreApi<object>>>(store: S) {
const storeIn = store as WithSelectors<typeof store>;
storeIn.use = {} as any;
for (const key of Object.keys(storeIn.getState())) {
(storeIn.use as any)[key] = () => storeIn((s: any) => s[key]);
}
return storeIn;
}
// Usage
const useCounterStoreBase = create<CounterState>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
export const useCounterStore = createSelectors(useCounterStoreBase);
// Auto-generated selectors
const count = useCounterStore.use.count();
const increment = useCounterStore.use.increment();Memoized derived selector:
import { useMemo } from "react";
function ExpensiveList() {
const items = useCartStore((s) => s.items);
// Memoize expensive computation
const sortedItems = useMemo(
() => [...items].sort((a, b) => b.price - a.price),
[items]
);
return <ul>{sortedItems.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}Selecting actions (stable reference):
// Actions never change, so this selector never triggers a re-render
const increment = useCounterStore((s) => s.increment);
const reset = useCounterStore((s) => s.reset);useShallow preserves the return type of the selector.// result is typed as number
const count = useStore((s: StoreState) => s.count);
// result is typed as { name: string; email: string }
const user = useStore(
useShallow((s: StoreState) => ({ name: s.user.name, email: s.user.email }))
);useShallow causes re-renders on every state change: (s) => ({ a: s.a, b: s.b }) creates a new object each time.useShallow only compares one level deep. Nested objects or arrays still use reference equality for their children.(s) => s.increment never triggers re-renders because function references are stable in Zustand. This is safe and recommended.useMemo after selecting the raw data instead.| Approach | Pros | Cons |
|---|---|---|
| Individual selectors | Precise, minimal re-renders | Verbose for many values |
| useShallow | Multi-value selection, simple API | Only shallow comparison |
| Auto-generated selectors | Zero boilerplate per-field access | Magic, harder to debug |
| No selector (full store) | Simple | Re-renders on every change |
===) to compare the previous and next selected value.{} !== {}.useShallow when your selector returns an object or array with multiple values.zustand/react/shallow.import { useShallow } from "zustand/react/shallow";
const { name, avatar } = useStore(
useShallow((s) => ({ name: s.user.name, avatar: s.user.avatar }))
);{} !== {}, Zustand sees it as a new value and triggers a re-render.useShallow to compare individual properties instead.useShallow only compares one level deep.createSelectors wrapper generates per-field hooks: useStore.use.count().useMemo for expensive derived computations.const items = useCartStore((s) => s.items);
const sorted = useMemo(() => [...items].sort(...), [items]);if block or after a conditional return.useShallow preserves the selector's return type.// result is typed as number
const count = useStore((s: StoreState) => s.count);type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;useShallow selects multiple values but adds a shallow comparison pass.Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥