Zustand stores, client state patterns. Use when deciding between Zustand vs useState, managing global state, avoiding Context misuse, or handling form state.
Quick Guide: Zustand owns state two or more components read;
useStateowns state one component reads; the URL owns filters and search; Context injects singletons and holds no state. Zustand v5 changes three things:useShallowfromzustand/react/shallowreplaces the old equality-function second argument, selectors must return stable references or they loop, andpersistno longer captures initial state at creation.
Detailed Resources:
useShallow, the Context anti-pattern, URL state<critical_requirements>
Move a value into a store as soon as a second component reads it. Passing it down instead couples every component on the path, and relocating it later means unpicking that chain.
Select one value per useStore call. The component then re-renders when that value changes and at no other time; a call with no selector subscribes to the whole store.
Return a stable reference from every selector. An object or function built inside the selector is a new reference on each call, which in v5 re-renders without end.
Persist preferences and nothing else, through partialize. Transient UI restored from storage — a modal that reopens itself, a sidebar that remembers being closed — reads as a bug.
Keep server data out of the store. Cached, invalidated, refetched data wants a layer built for it; a store gives you a copy that goes stale silently.
</critical_requirements>
Auto-detection: create from zustand, zustand/middleware, zustand/react/shallow, useShallow, createWithEqualityFn, zustand/traditional, persist, partialize, devtools, store slices
Applies to:
useState, the URL and Context for a given valuedevtools and persistHandled elsewhere:
Zustand is a subscription primitive with a hook attached. Its performance comes entirely from the selector: the store notifies every subscriber on every change, and the selector is what decides whether that notification becomes a render. So store design is selector design.
toggleSidebar(); the store decides what toggling means<decision_framework>
| The value | Belongs in | Because |
| ----------------------------- | -------------- | ------------------------------------------------------------ |
| Read by 2+ components | a store | selective re-renders, and no prop chain to unpick later |
| Read by 1 component | useState | nothing else can observe it, so nothing else needs to |
| A filter, query, page or sort | the URL | shareable, bookmarkable, and the back button works |
| A singleton set at startup | Context | it never changes, so the re-render cost never arises |
| Fetched from an API | not this skill | it needs caching and invalidation, which a store has none of |
Context is the row people get wrong. It is a transport for a value that does not change, and using it for state that does re-renders every consumer on every change with no way to opt out.
</decision_framework>
devtools for inspection, persist for the fields that should outlive the tab, partialize to keep everything else out of storage.
export const useUIStore = create<UIState>()(
devtools(
persist(
(set) => ({
theme: DEFAULT_THEME,
sidebarOpen: true,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}),
{ name: UI_STORAGE_KEY, partialize: (state) => ({ theme: state.theme }) },
),
),
);
Full code: examples/core.md
One value per call is the default, and the reason the store is fast.
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
const isOpen = useUIStore((state) => state.sidebarOpen);
Full code: examples/core.md
At three or more values from one store, one useShallow call beats three subscriptions.
import { useShallow } from "zustand/react/shallow";
const { sidebarOpen, theme } = useUIStore(
useShallow((state) => ({
sidebarOpen: state.sidebarOpen,
theme: state.theme,
})),
);
Full code: examples/core.md
Context carries a value that never changes — a client, a connection, a configuration read at startup.
const DatabaseContext = createContext<Database | null>(null);
Full code: examples/core.md
Filters, search, pagination and sort live in the URL, where they survive a reload and a paste into a colleague's chat.
const searchParams = new URLSearchParams(window.location.search);
const category = searchParams.get("category") ?? undefined;
Full code: examples/core.md
</patterns><red_flags>
Breaks at runtime:
create() called with a second equality-function argument — removed in v5, so the argument is ignored and the comparison silently never happens. Use useShallow, or createWithEqualityFn from zustand/traditional.persist did not store it. Set it with setState after creation.Surprising behaviour:
useStore() with no selector subscribes to the entire store, so the component re-renders on changes to fields it never reads.useShallow compares the object, not the individual reads.?page=2 reads back as "2", and ?open=false is truthy until parsed.</red_flags>
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer