The Zustand skill provides expert knowledge for client-side state management using the decoupled actions pattern . Learn how to create stores with proper TypeScript types, atomic selectors, and tree-shakeable actions.
Overview
This skill teaches:
Decoupled actions - Export actions as plain functions, not in the store
Atomic selectors - Select minimal state to prevent unnecessary re-renders
Client-side only - Store modules must only be imported from Client Components
useState vs Zustand - When to use each approach
Zustand stores are client-side only. Never import store modules from Server Components.
Quick Start
Install Zustand
Create Store
Use in Component
npm install zustand
# or
bun add zustand
Core Principles
1. Client-Side Only
❌ Server Component Import
✅ Pass Data from Server
// app/page.tsx (Server Component)
import { useCounterStore } from "@/store/use-counter-store"
// ❌ This will fail - Server Components cannot use client state
export default function Page () {
return < div > ... </ div >
}
2. State Only in Store
The create() call should define the state shape and initial values only. Actions are defined separately.
❌ Actions Inside create()
✅ Decoupled Actions
export const useStore = create < State & Actions >(( set ) => ({
count: 0 ,
increment : () => set (( state ) => ({ count: state . count + 1 })),
}))
3. Atomic Selectors
Select the smallest slice of state needed to minimize re-renders.
❌ Selecting Entire State
✅ Atomic Selector
const state = useStore ()
// Re-renders on ANY state change
The Decoupled Actions Pattern
Why This Pattern?
No hook for actions - Components import actions directly, avoiding unnecessary subscriptions
Testable - Actions are plain functions that can be tested in isolation
Tree-shakeable - Unused actions are eliminated from the bundle
Full Example
Store Definition
Component Usage
// web/store/use-todo-store.ts
import { create } from "zustand"
import { subscribeWithSelector } from "zustand/middleware"
interface Todo {
id : string
text : string
completed : boolean
}
interface TodoState {
todos : Todo []
filter : 'all' | 'active' | 'completed'
}
const initialState : TodoState = {
todos: [],
filter: 'all'
}
export const useTodoStore = create < TodoState >()()
subscribeWithSelector (() => initialState )
)
// Actions
export const addTodo = ( text : string ) => {
useTodoStore . setState (( state ) => ({
todos: [
... state . todos ,
{ id: crypto . randomUUID (), text , completed: false }
]
}))
}
export const toggleTodo = ( id : string ) => {
useTodoStore . setState (( state ) => ({
todos: state . todos . map (( todo ) =>
todo . id === id ? { ... todo , completed: ! todo . completed } : todo
)
}))
}
export const deleteTodo = ( id : string ) => {
useTodoStore . setState (( state ) => ({
todos: state . todos . filter (( todo ) => todo . id !== id )
}))
}
export const setFilter = ( filter : TodoState [ 'filter' ]) => {
useTodoStore . setState ({ filter })
}
export const clearCompleted = () => {
useTodoStore . setState (( state ) => ({
todos: state . todos . filter (( todo ) => ! todo . completed )
}))
}
export const reset = () => {
useTodoStore . setState ( initialState )
}
Best Practices
Always Use subscribeWithSelector
import { create } from "zustand"
import { subscribeWithSelector } from "zustand/middleware"
export const useMyStore = create < MyStore >()()
subscribeWithSelector (( set , get ) => ({
// state...
}))
)
Use Functional Updates
When the new value depends on previous state:
✅ Correct: Functional Update
❌ Avoid: Reading Outside setState
export const increment = () => {
useStore . setState (( state ) => ({ count: state . count + 1 }))
}
Multiple Selectors
When you need multiple values:
Option 1: Multiple Atomic Selectors
Option 2: useShallow for Object Selection
const count = useStore (( state ) => state . count )
const items = useStore (( state ) => state . items )
useState vs Zustand
Use Zustand When
Use useState When
State needs to be shared across multiple components
State persists between component unmounts
Form state that needs to be accessed from multiple places
UI state that should survive navigation
Complex state with many actions
State is truly local to a single component
Simple boolean toggles (like isOpen)
Temporary input values before submission
One-off UI state that doesn’t justify a store
Default to Zustand for most state, even local UI state. This keeps components presentational and state testable. Only use useState for trivial, ephemeral toggles.
Store Template
Use this template for new stores:
// web/store/use-{{name}}-store.ts
import { create } from "zustand"
import { subscribeWithSelector } from "zustand/middleware"
// ============================================================================
// Types
// ============================================================================
interface {{ Name }} State {
items : unknown []
selectedId : string | null
isLoading : boolean
error : string | null
}
// ============================================================================
// Initial State
// ============================================================================
const initialState : {{ Name }} State = {
items: [],
selectedId: null ,
isLoading: false ,
error: null ,
}
// ============================================================================
// Store
// ============================================================================
export const use {{Name}} Store = create < {{ Name }} State > ()()
subscribeWithSelector (() => initialState )
)
// ============================================================================
// Actions (decoupled)
// ============================================================================
export const setItems = ( items : unknown []) => {
use {{ Name }} Store . setState ({ items })
}
export const setSelectedId = ( selectedId : string | null ) => {
use {{ Name }} Store . setState ({ selectedId })
}
export const setLoading = ( isLoading : boolean ) => {
use {{ Name }} Store . setState ({ isLoading })
}
export const setError = ( error : string | null ) => {
use {{ Name }} Store . setState ({ error })
}
export const reset = () => {
use {{ Name }} Store . setState ( initialState )
}
See .github/skills/zustand/assets/template.md for the full template.
Quality Checklist
Before completing any Zustand-related task:
Skill Structure
.github/skills/zustand/
├── SKILL.md # This overview
└── assets/
└── template.md # Store template
References
Copy the template from .github/skills/zustand/assets/template.md and replace {{Name}} and {{description}} placeholders when creating new stores.