About zustand-state-management
A development skill that provides conventions for using Zustand v5 in React applications. It focuses on small stores, custom hooks, selector patterns, and separating client UI state from server and URL state.
Key features
- 01Zustand v5 store conventions
- 02Custom hook export patterns
- 03Atomic selector guidance
- 04Event-style action design
- 05Server and URL state boundaries
Use cases
- 01Sharing UI state across components
- 02Storing temporary client preferences
- 03Combining Zustand state with TanStack Query
- 04Structuring feature-specific stores
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ fixmyberlin/ fixmyskills --skill zustand-state-managementAfter installing, type/zustand-state-managementin Claude Code to confirm it loaded.
Details
From the project READMELLM reference: Fetch llms.txt for the documentation index and latest API. Full docs: llms-full.txt. Human-readable docs: zustand.docs.pmnd.rs.
Core mental model: Working with Zustand (Dominik Dorfmeister) — export custom hooks only, atomic selectors, actions namespace, event-style actions, many small stores, combine with Query/URL via custom hooks.
Version: Zustand v5. Pin zustand@^5 in the app; check migrating to v5 when upgrading.
When to use Zustand
| Use Zustand for | Use something else for |
|---|---|
| Client UI state shared across components (panels, map tools, wizard steps) | Server data → TanStack Query |
| Ephemeral client prefs not in the URL | Shareable/URL state → router search params (Vite app without Router: nuqs); never duplicate it in Zustand |
| Small, synchronous client state | Auth session → skill tanstack-start-auth |
Most app state is server or URL state. Add a Zustand store only when you need client-global state that is not already covered.
File and export naming
One store file = one concern. Colocate with the feature (see tanstack-start-conventions for Start layout; SPA apps: feature folders under components/ / shared/).
| Item | Convention | Example |
|---|---|---|
| File | {domain}-store.ts (kebab-case, -store suffix) | map-filters-store.ts |
| Folder | Next to the feature, e.g. src/features/map/ | not a single global stores/ dump unless shared |
| State type | {Domain}Store (PascalCase interface) | MapFiltersStore |
Internal create hook | use{Domain}Store — never exported | useMapFiltersStore |
| Exported hooks | use{Slice} for state primitives; use{Domain}Actions for actions | useAppliedFilters, useMapFiltersActions |
Do not use: store.ts, state.ts, zustand.ts, or useMapFiltersStore.ts (filename must not imply exporting the raw store). Do not put unrelated domains in one file.
Persist: unique name per store, aligned with the file, e.g. fmc-map-filters for map-filters-store.ts. TanStack Start SSR + persist: follow skill tanstack-start-conventions and llms.txt → SSR and Hydration (hydration gate or official SSR-safe patterns).
Canonical store shape
import { create } from 'zustand'
interface MapFiltersStore {
applied: string[]
actions: {
addFilter: (filter: string) => void
clearFilters: () => void
}
}
const useMapFiltersStore = create<MapFiltersStore>()((set) => ({
applied: [],
actions: {
addFilter: (filter) =>
set((state) => (state.applied.includes(filter) ? state : { applied: [...state.applied, filter] })),
clearFilters: () => set({ applied: [] }),
},
}))
export const useAppliedFilters = () => useMapFiltersStore((state) => state.applied)
export const useMapFiltersActions = () => useMapFiltersStore((state) => state.actions)
TypeScript: always create<StoreType>()((set) => …) (double parentheses), even without middleware.
Rules
- Only export custom hooks (
use{Slice}+use{Domain}Actions); keepuse{Domain}Storeprivate. Never call a store hook without a selector. - Atomic selectors returning primitives or stable references; one exported hook per slice. Reach for
useShallow(fromzustand/shallow) only when two values are rarely needed together, and prefer atomic hooks when practical. - Separate
actionsfrom state. Theactionsreference is stable, soconst { addFilter } = useMapFiltersActions()is fine. - Model actions as events, not setters:
addFilter,closePanel, notsetAppliedcalled from components. Business logic stays in the store. For high-frequency inputs (resize, observers), return the previousstatefromsetwhen nothing changed. - Many small stores over one app-wide store. Combine at the edges with custom hooks (e.g. a hook reading
useCredentialsStore+useUsersStore). Avoid the slices pattern unless there is a strong reason (heavy TypeScript; see llms.txt). - Combine with TanStack Query via custom hooks: filters live in Zustand, the server list in Query (
queryKey: ['todos', filters]). - No Zustand for server-fetched data.
Delegate to llms.txt
Do not duplicate full API docs in this skill. Fetch llms.txt for middleware (persist, devtools, immer; persist imports from zustand/middleware), patterns (slices, reset state, vanilla store, testing), integrations (SSR and Hydration, URL hash, Maps/Sets), and v4 → v5 migration.
Related skills
| Topic | Skill |
|---|---|
| App layout, SSR (Start) | tanstack-start-conventions |
| URL search (Router) | tanstack-router-conventions |
| URL query state (no Router) | nuqs |