
Zustand State Management
- 187 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Helps with ai & agent building tasks.
About
zustand-state-management is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- zustand-state-management
- AI & Agent Building
- AI-coding skill
Zustand State Management by the numbers
- 187 all-time installs (skills.sh)
- +20 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,995 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill zustand-state-managementAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 187 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Zustand State Management
Status: Production Ready ✅ Last Updated: 2025-11-21 Latest Version: zustand@5.0.8 Dependencies: React 18+, TypeScript 5+
---
Quick Start (3 Minutes)
1. Install Zustand
bun add zustand # preferred
# or: npm install zustand
# or: yarn add zustandWhy Zustand?
- Minimal API: Only 1 function to learn (
create) - No boilerplate: No providers, reducers, or actions
- TypeScript-first: Excellent type inference
- Fast: Fine-grained subscriptions prevent unnecessary re-renders
- Flexible: Middleware for persistence, devtools, and more
2. Create Your First Store (TypeScript)
import { create } from 'zustand'
interface BearStore {
bears: number
increase: (by: number) => void
reset: () => void
}
const useBearStore = create<BearStore>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
reset: () => set({ bears: 0 }),
}))CRITICAL: Notice the double parentheses create<T>()() - this is required for TypeScript with middleware.
3. Use Store in Components
import { useBearStore } from './store'
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here...</h1>
}
function Controls() {
const increase = useBearStore((state) => state.increase)
return <button onClick={() => increase(1)}>Add bear</button>
}Why this works:
- Components only re-render when their selected state changes
- No Context providers needed
- Selector function extracts specific state slice
---
The 3-Pattern Setup Process
Pattern 1: Basic Store (JavaScript)
For simple use cases without TypeScript:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))When to use:
- Prototyping
- Small apps
- No TypeScript in project
Pattern 2: TypeScript Store (Recommended)
For production apps with type safety:
import { create } from 'zustand'
// Define store interface
interface CounterStore {
count: number
increment: () => void
decrement: () => void
}
// Create typed store
const useCounterStore = create<CounterStore>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))Key Points:
- Separate interface for state + actions
- Use
create<T>()()syntax (currying for middleware) - Full IDE autocomplete and type checking
Pattern 3: Persistent Store
For state that survives page reloads:
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface UserPreferences {
theme: 'light' | 'dark' | 'system'
language: string
setTheme: (theme: UserPreferences['theme']) => void
setLanguage: (language: string) => void
}
const usePreferencesStore = create<UserPreferences>()(
persist(
(set) => ({
theme: 'system',
language: 'en',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'user-preferences', // unique name in localStorage
storage: createJSONStorage(() => localStorage), // optional: defaults to localStorage
},
),
)Why this matters:
- State automatically saved to localStorage
- Restored on page reload
- Works with sessionStorage too
- Handles serialization automatically
---
Critical Rules
Always Do
✅ Use create<T>()() (double parentheses) in TypeScript for middleware compatibility ✅ Define separate interfaces for state and actions ✅ Use selector functions to extract specific state slices ✅ Use set with updater functions for derived state: set((state) => ({ count: state.count + 1 })) ✅ Use unique names for persist middleware storage keys ✅ Handle Next.js hydration with hasHydrated flag pattern ✅ Use shallow for selecting multiple values ✅ Keep actions pure (no side effects except state updates)
Never Do
❌ Use create<T>(...) (single parentheses) in TypeScript - breaks middleware types ❌ Mutate state directly: set((state) => { state.count++; return state }) - use immutable updates ❌ Create new objects in selectors: useStore((state) => ({ a: state.a })) - causes infinite renders ❌ Use same storage name for multiple stores - causes data collisions ❌ Access localStorage during SSR without hydration check ❌ Use Zustand for server state - use TanStack Query instead ❌ Export store instance directly - always export the hook
---
Known Issues Prevention (5 Issues)
| Issue | Error | Quick Fix |
|---|---|---|
| #1 Hydration mismatch | "Text content does not match" | Use _hasHydrated flag + onRehydrateStorage |
| #2 TypeScript inference | Types break with middleware | Use create<T>()() double parentheses |
| #3 Import error | "createJSONStorage not exported" | Upgrade to zustand@5.0.8+ |
| #4 Infinite loop | Browser freezes | Use shallow or separate selectors |
| #5 Slices types | StateCreator types fail | Explicit StateCreator<Combined, [], [], Slice> |
Most Critical - TypeScript double parentheses:
// ❌ WRONG: create<T>((set) => ...)
// ✅ CORRECT: create<T>()((set) => ...)See: references/known-issues.md for complete solutions with code examples.
---
Middleware Configuration
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useStore = create<MyStore>()(
devtools(
persist(
(set) => ({ /* store definition */ }),
{ name: 'my-storage' },
),
{ name: 'MyStore' },
),
)| Middleware | Purpose | Import |
|---|---|---|
persist | localStorage/sessionStorage | zustand/middleware |
devtools | Redux DevTools integration | zustand/middleware |
immer | Mutable update syntax | zustand/middleware/immer |
Order matters: devtools(persist(...)) shows persist actions in DevTools.
See: references/middleware-guide.md for complete middleware documentation.
---
Common Patterns
| Pattern | Use Case | Key Technique |
|---|---|---|
| Computed values | Derived data | Compute in selector: state.items.length |
| Async actions | API calls | set({ isLoading: true }) + try/catch |
| Reset store | Logout, form clear | set(initialState) |
| Selector with params | Dynamic access | state.todos.find(t => t.id === id) |
| Multiple stores | Separation of concerns | Create separate create() calls |
See: references/common-patterns.md for complete implementations.
---
Advanced Topics
| Topic | Use Case | Key API |
|---|---|---|
| Vanilla store | Non-React, testing | createStore() from zustand/vanilla |
| Custom middleware | Logging, timestamps | Wrap StateCreator |
| Immer | Mutable update syntax | immer() middleware |
| Subscriptions | Side effects | store.subscribe() |
See: references/advanced-topics.md for complete implementations.
---
Bundled Resources
| Type | Files |
|---|---|
| Templates | basic-store.ts, typescript-store.ts, persist-store.ts, slices-pattern.ts, devtools-store.ts, nextjs-store.ts, computed-store.ts, async-actions-store.ts |
| References | middleware-guide.md, typescript-patterns.md, nextjs-hydration.md, migration-guide.md, known-issues.md, common-patterns.md, advanced-topics.md |
---
When to Load References
| Reference | Load When... |
|---|---|
known-issues.md | Debugging hydration, TypeScript, infinite loop, or slices errors |
common-patterns.md | Implementing computed values, async actions, reset patterns |
advanced-topics.md | Vanilla stores, custom middleware, Immer, subscriptions |
middleware-guide.md | Configuring persist, devtools, or combining middlewares |
typescript-patterns.md | Complex type inference issues, StateCreator problems |
nextjs-hydration.md | Next.js SSR/hydration problems |
migration-guide.md | Migrating from Redux, Context API, or Zustand v4 |
---
Quick Troubleshooting
| Problem | Solution |
|---|---|
| Store updates don't trigger re-renders | Use selector: useStore(state => state.value) not destructuring |
| TypeScript errors with middleware | Use create<T>()() double parentheses |
| Hydration error with persist | Implement _hasHydrated flag pattern |
| Actions not showing in DevTools | Pass action name: set(newState, undefined, 'actionName') |
| Store resets unexpectedly | HMR causes reset in development |
---
Dependencies
{ "dependencies": { "zustand": "^5.0.8", "react": "^18.0.0+" } }Compatibility: React 18+, React 19, TypeScript 5+, Next.js 14+, Vite 5+
---
Official Docs: https://zustand.docs.pmnd.rs/ | GitHub: https://github.com/pmndrs/zustand
[TODO: Example Template File]
[TODO: This directory contains files that will be used in the OUTPUT that Claude produces.]
[TODO: Examples:]
- Templates (.html, .tsx, .md)
- Images (.png, .svg)
- Fonts (.ttf, .woff)
- Boilerplate code
- Configuration file templates
[TODO: Delete this file and add your actual assets]
These files are NOT loaded into context. They are copied or used directly in the final output.
Advanced Zustand Topics
This reference covers advanced Zustand patterns for complex use cases.
---
Vanilla Store (Without React)
Use Zustand outside React for Node.js, tests, or framework-agnostic code.
import { createStore } from 'zustand/vanilla'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
}
const store = createStore<CounterStore>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// Subscribe to changes
const unsubscribe = store.subscribe((state, prevState) => {
console.log('Count changed:', prevState.count, '->', state.count)
})
// Get current state
console.log(store.getState().count) // 0
// Update state
store.getState().increment()
console.log(store.getState().count) // 1
// Direct set
store.setState({ count: 10 })
// Cleanup
unsubscribe()Using Vanilla Store in React
import { createStore, useStore } from 'zustand'
// Create vanilla store
const counterStore = createStore<CounterStore>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
// Create React hook
function useCounterStore<T>(selector: (state: CounterStore) => T): T {
return useStore(counterStore, selector)
}
// Use in component
function Counter() {
const count = useCounterStore((state) => state.count)
return <div>{count}</div>
}---
Custom Middleware
Create reusable middleware for cross-cutting concerns.
Logger Middleware
import { StateCreator, StoreMutatorIdentifier } from 'zustand'
type Logger = <T>(
f: StateCreator<T, [], []>,
name?: string,
) => StateCreator<T, [], []>
const logger: Logger = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...a) => {
const before = get()
set(...(a as Parameters<typeof set>))
const after = get()
console.log(`[${name ?? 'store'}]`, { before, after })
}
return f(loggedSet, get, store)
}
// Use custom middleware
const useStore = create<MyStore>()(
logger((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}), 'CounterStore'),
)Timestamp Middleware
type WithTimestamp<T> = T & { _lastUpdated: number }
const timestamp = <T>(f: StateCreator<T, [], []>): StateCreator<WithTimestamp<T>, [], []> =>
(set, get, store) => ({
...f(
(partial, replace) => {
set(
(state) => ({
...(typeof partial === 'function' ? partial(state as T) : partial),
_lastUpdated: Date.now(),
}) as WithTimestamp<T>,
replace,
)
},
get as () => T,
store,
),
_lastUpdated: Date.now(),
})---
Immer Middleware (Mutable Updates)
Write mutable update syntax that becomes immutable under the hood.
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface TodoStore {
todos: Array<{ id: string; text: string; done: boolean }>
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
}
const useStore = create<TodoStore>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
// Mutate directly with Immer
state.todos.push({ id: Date.now().toString(), text, done: false })
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) todo.done = !todo.done
}),
removeTodo: (id) =>
set((state) => {
const index = state.todos.findIndex((t) => t.id === id)
if (index !== -1) state.todos.splice(index, 1)
}),
})),
)When to use Immer:
- Deeply nested state updates
- Complex array manipulations
- Team familiarity with mutable patterns
When NOT to use Immer:
- Simple state
- Performance-critical apps (Immer adds overhead)
- Small bundle size requirements
---
Subscriptions and Side Effects
React to state changes outside components.
// Subscribe to specific state changes
const unsubscribe = useStore.subscribe(
(state) => state.count,
(count, prevCount) => {
console.log('Count changed from', prevCount, 'to', count)
// Trigger side effect
if (count > 10) {
analytics.track('count_exceeded_10')
}
},
)
// Subscribe with selector and equality function
import { shallow } from 'zustand/shallow'
useStore.subscribe(
(state) => ({ count: state.count, name: state.name }),
(selection, prevSelection) => {
console.log('Selection changed:', selection)
},
{ equalityFn: shallow },
)Transient Updates (No Re-render)
Update state without triggering re-renders.
const useStore = create<{
x: number
y: number
setPosition: (x: number, y: number) => void
}>()((set) => ({
x: 0,
y: 0,
setPosition: (x, y) => set({ x, y }, true), // Replace, don't merge
}))
// Subscribe to track position without React re-renders
const unsubscribe = useStore.subscribe(console.log)---
Testing Zustand Stores
Unit Testing Stores
import { act, renderHook } from '@testing-library/react'
import { useCounterStore } from './counterStore'
describe('counterStore', () => {
beforeEach(() => {
// Reset store between tests
useCounterStore.setState({ count: 0 })
})
it('should increment count', () => {
const { result } = renderHook(() => useCounterStore())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
it('should decrement count', () => {
useCounterStore.setState({ count: 5 })
const { result } = renderHook(() => useCounterStore())
act(() => {
result.current.decrement()
})
expect(result.current.count).toBe(4)
})
})Testing with Persist Middleware
import { usePersistedStore } from './persistedStore'
describe('persistedStore', () => {
beforeEach(() => {
// Clear localStorage
localStorage.clear()
// Reset store
usePersistedStore.setState({ value: '' })
})
it('should persist to localStorage', async () => {
const { result } = renderHook(() => usePersistedStore())
act(() => {
result.current.setValue('test')
})
// Wait for persist
await new Promise((r) => setTimeout(r, 0))
expect(localStorage.getItem('persisted-store')).toContain('test')
})
})---
Performance Optimization
Fine-Grained Selectors
// ❌ Subscribes to entire store
const { count, name, items } = useStore()
// ✅ Subscribes only to what's needed
const count = useStore((state) => state.count)
const name = useStore((state) => state.name)Memoized Selectors
import { useMemo } from 'react'
function ExpensiveList() {
const items = useStore((state) => state.items)
// Memoize expensive computation
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items],
)
return <ul>{sortedItems.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
}Batched Updates
const useStore = create<MyStore>()((set) => ({
count: 0,
name: '',
// Batch multiple updates
updateBoth: (count: number, name: string) =>
set({ count, name }), // Single update, single re-render
}))---
Summary
| Topic | Use Case | Key API |
|---|---|---|
| Vanilla store | Non-React, testing | createStore() |
| Custom middleware | Logging, timestamps | StateCreator wrapping |
| Immer | Complex nested updates | immer() middleware |
| Subscriptions | Side effects | store.subscribe() |
| Testing | Unit tests | setState(), renderHook() |
| Performance | Optimization | Fine-grained selectors |
---
See Also
middleware-guide.md- Built-in middleware detailstypescript-patterns.md- TypeScript-specific patterns- Official docs: https://zustand.docs.pmnd.rs/
Common Zustand Patterns - Detailed Examples
This reference provides complete implementations for common Zustand patterns.
---
Pattern: Computed/Derived Values
Compute values in selectors, not in the store itself.
interface StoreWithComputed {
items: string[]
addItem: (item: string) => void
// Computed in selector, not stored
}
const useStore = create<StoreWithComputed>()((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}))
// Use in component
function ItemCount() {
const count = useStore((state) => state.items.length)
return <div>{count} items</div>
}
// More complex computed values
function ExpensiveItems() {
const expensive = useStore((state) =>
state.items.filter((item) => item.startsWith('expensive'))
)
return <div>{expensive.length} expensive items</div>
}Key Points:
- Compute in selector, not store
- Memoize expensive computations with
useMemoif needed - Selectors run on every render - keep them fast
---
Pattern: Async Actions
Handle asynchronous operations with loading and error states.
interface AsyncStore {
data: string | null
isLoading: boolean
error: string | null
fetchData: () => Promise<void>
}
const useAsyncStore = create<AsyncStore>()((set) => ({
data: null,
isLoading: false,
error: null,
fetchData: async () => {
set({ isLoading: true, error: null })
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to fetch')
const data = await response.text()
set({ data, isLoading: false })
} catch (error) {
set({ error: (error as Error).message, isLoading: false })
}
},
}))
// Use in component
function DataDisplay() {
const { data, isLoading, error, fetchData } = useAsyncStore()
useEffect(() => {
fetchData()
}, [fetchData])
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{data}</div>
}Best Practice: For complex server state, consider TanStack Query instead.
---
Pattern: Resetting Store
Reset store to initial values (useful for logout, form reset, etc.).
interface ResettableStore {
count: number
name: string
increment: () => void
setName: (name: string) => void
reset: () => void
}
const initialState = {
count: 0,
name: '',
}
const useStore = create<ResettableStore>()((set) => ({
...initialState,
increment: () => set((state) => ({ count: state.count + 1 })),
setName: (name) => set({ name }),
reset: () => set(initialState),
}))
// Use in component
function LogoutButton() {
const reset = useStore((state) => state.reset)
const handleLogout = () => {
// Clear user session
reset() // Reset store to initial state
}
return <button onClick={handleLogout}>Logout</button>
}---
Pattern: Selector with Params
Create parameterized selectors for dynamic data access.
interface TodoStore {
todos: Array<{ id: string; text: string; done: boolean }>
addTodo: (text: string) => void
toggleTodo: (id: string) => void
}
const useStore = create<TodoStore>()((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: Date.now().toString(), text, done: false }],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
),
})),
}))
// Use with parameter
function Todo({ id }: { id: string }) {
const todo = useStore((state) => state.todos.find((t) => t.id === id))
const toggleTodo = useStore((state) => state.toggleTodo)
if (!todo) return null
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(id)}
/>
{todo.text}
</div>
)
}
// Filtered todos
function CompletedTodos() {
const completedTodos = useStore((state) =>
state.todos.filter((t) => t.done)
)
return <div>{completedTodos.length} completed</div>
}---
Pattern: Multiple Stores
Separate concerns with multiple independent stores.
// userStore.ts
interface UserStore {
user: { id: string; name: string } | null
setUser: (user: UserStore['user']) => void
logout: () => void
}
export const useUserStore = create<UserStore>()((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}))
// cartStore.ts
interface CartStore {
items: Array<{ id: string; quantity: number }>
addItem: (id: string) => void
removeItem: (id: string) => void
clearCart: () => void
}
export const useCartStore = create<CartStore>()((set) => ({
items: [],
addItem: (id) =>
set((state) => {
const existing = state.items.find((i) => i.id === id)
if (existing) {
return {
items: state.items.map((i) =>
i.id === id ? { ...i, quantity: i.quantity + 1 } : i
),
}
}
return { items: [...state.items, { id, quantity: 1 }] }
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
clearCart: () => set({ items: [] }),
}))
// Use in components
function Header() {
const user = useUserStore((state) => state.user)
const itemCount = useCartStore((state) => state.items.length)
return (
<header>
<span>{user?.name ?? 'Guest'}</span>
<span>Cart: {itemCount}</span>
</header>
)
}---
Pattern: Cross-Store Actions
Access one store from another for complex workflows.
// Use getState() to access store outside React
const useAuthStore = create<AuthStore>()((set) => ({
isAuthenticated: false,
login: () => set({ isAuthenticated: true }),
logout: () => {
set({ isAuthenticated: false })
// Clear cart when logging out
useCartStore.getState().clearCart()
},
}))---
Summary Table
| Pattern | Use Case | Key Function |
|---|---|---|
| Computed values | Derived data | Selector functions |
| Async actions | API calls | set with loading/error states |
| Reset store | Logout, form clear | set(initialState) |
| Selector with params | Dynamic access | state.items.find(id) |
| Multiple stores | Separation of concerns | Create separate stores |
| Cross-store actions | Complex workflows | otherStore.getState() |
---
See Also
- Main SKILL.md for basic patterns
templates/for copy-paste implementationstypescript-patterns.mdfor advanced typing
[TODO: Reference Document Name]
[TODO: This file contains reference documentation that Claude can load when needed.]
[TODO: Delete this file if you don't have reference documentation to provide.]
Purpose
[TODO: Explain what information this document contains]
When Claude Should Use This
[TODO: Describe specific scenarios where Claude should load this reference]
Content
[TODO: Add your reference content here - schemas, guides, specifications, etc.]
---
Note: This file is NOT loaded into context by default. Claude will only load it when:
- It determines the information is needed
- You explicitly ask Claude to reference it
- The SKILL.md instructions direct Claude to read it
Keep this file under 10k words for best performance.
Known Issues Prevention - Detailed Solutions
This reference covers the 5 most common Zustand issues with complete solutions.
---
Issue #1: Next.js Hydration Mismatch
Error: "Text content does not match server-rendered HTML" or "Hydration failed"
Source:
- DEV Community: Persist middleware in Next.js
- GitHub Discussions #2839
Why It Happens: Persist middleware reads from localStorage on client but not on server, causing state mismatch.
Prevention:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface StoreWithHydration {
count: number
_hasHydrated: boolean
setHasHydrated: (hydrated: boolean) => void
increase: () => void
}
const useStore = create<StoreWithHydration>()(
persist(
(set) => ({
count: 0,
_hasHydrated: false,
setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
increase: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'my-store',
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true)
},
},
),
)
// In component
function MyComponent() {
const hasHydrated = useStore((state) => state._hasHydrated)
if (!hasHydrated) {
return <div>Loading...</div>
}
// Now safe to render with persisted state
return <ActualContent />
}---
Issue #2: TypeScript Double Parentheses Missing
Error: Type inference fails, StateCreator types break with middleware
Source: Official Zustand TypeScript Guide
Why It Happens: The currying syntax create<T>()() is required for middleware to work with TypeScript inference.
Prevention:
// ❌ WRONG - Single parentheses
const useStore = create<MyStore>((set) => ({
// ...
}))
// ✅ CORRECT - Double parentheses
const useStore = create<MyStore>()((set) => ({
// ...
}))Rule: Always use create<T>()() in TypeScript, even without middleware (future-proof).
---
Issue #3: Persist Middleware Import Error
Error: "Attempted import error: 'createJSONStorage' is not exported from 'zustand/middleware'"
Source: GitHub Discussion #2839
Why It Happens: Wrong import path or version mismatch between zustand and build tools.
Prevention:
// ✅ CORRECT imports for v5
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
// Verify versions
// zustand@5.0.8 includes createJSONStorage
// zustand@4.x uses different API
// Check your package.json
// "zustand": "^5.0.8"---
Issue #4: Infinite Render Loop
Error: Component re-renders infinitely, browser freezes
Source: GitHub Discussions #2642
Why It Happens: Creating new object references in selectors causes Zustand to think state changed.
Prevention:
import { shallow } from 'zustand/shallow'
// ❌ WRONG - Creates new object every time
const { bears, fishes } = useStore((state) => ({
bears: state.bears,
fishes: state.fishes,
}))
// ✅ CORRECT Option 1 - Select primitives separately
const bears = useStore((state) => state.bears)
const fishes = useStore((state) => state.fishes)
// ✅ CORRECT Option 2 - Use shallow for multiple values
const { bears, fishes } = useStore(
(state) => ({ bears: state.bears, fishes: state.fishes }),
shallow,
)---
Issue #5: Slices Pattern TypeScript Complexity
Error: StateCreator types fail to infer, complex middleware types break
Source: Official Slices Pattern Guide
Why It Happens: Combining multiple slices requires explicit type annotations for middleware compatibility.
Prevention:
import { create, StateCreator } from 'zustand'
// Define slice types
interface BearSlice {
bears: number
addBear: () => void
}
interface FishSlice {
fishes: number
addFish: () => void
}
// Create slices with proper types
const createBearSlice: StateCreator<
BearSlice & FishSlice, // Combined store type
[], // Middleware mutators (empty if none)
[], // Chained middleware (empty if none)
BearSlice // This slice's type
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})
const createFishSlice: StateCreator<
BearSlice & FishSlice,
[],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
// Combine slices
const useStore = create<BearSlice & FishSlice>()((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))---
Quick Reference Table
| Issue | Error | Quick Fix |
|---|---|---|
| Hydration mismatch | "Text content does not match" | Use _hasHydrated flag pattern |
| TypeScript inference | Types break with middleware | Use create<T>()() double parentheses |
| Import error | "createJSONStorage not exported" | Upgrade to zustand@5.0.8+ |
| Infinite loop | Browser freezes | Use shallow or separate selectors |
| Slices types | StateCreator types fail | Explicit StateCreator<Combined, [], [], Slice> |
---
See Also
nextjs-hydration.md- More Next.js SSR patternstypescript-patterns.md- Advanced TypeScript solutionsmiddleware-guide.md- Middleware configuration details
Zustand Middleware Complete Guide
Complete reference for all Zustand middleware. Load when user asks about persistence, devtools, immer, or custom middleware.
---
Persist Middleware
Basic Usage
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<Store>()(
persist(
(set) => ({ /* store */ }),
{ name: 'storage-name' }
)
)Storage Options
// localStorage (default)
storage: createJSONStorage(() => localStorage)
// sessionStorage
storage: createJSONStorage(() => sessionStorage)
// Custom storage
storage: createJSONStorage(() => customStorage)Partial Persistence
persist(
(set) => ({ /* store */ }),
{
name: 'storage',
partialize: (state) => ({
theme: state.theme,
// Don't persist sensitive data
}),
}
)Schema Migration
persist(
(set) => ({ /* store */ }),
{
name: 'storage',
version: 2,
migrate: (persistedState: any, version) => {
if (version === 0) {
// v0 → v1
persistedState.newField = 'default'
}
if (version === 1) {
// v1 → v2
delete persistedState.oldField
}
return persistedState
},
}
)---
Devtools Middleware
Basic Usage
import { devtools } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
(set) => ({ /* store */ }),
{ name: 'StoreName' }
)
)Named Actions
increment: () => set(
(state) => ({ count: state.count + 1 }),
undefined,
'counter/increment' // Shows in DevTools
)Production Toggle
devtools(
(set) => ({ /* store */ }),
{
name: 'Store',
enabled: process.env.NODE_ENV === 'development'
}
)---
Immer Middleware
Allows mutable state updates (Immer handles immutability):
import { immer } from 'zustand/middleware/immer'
const useStore = create<Store>()(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
// Mutate directly!
state.todos.push({ id: Date.now(), text })
}),
}))
)When to use: Complex nested state updates
---
Combining Middlewares
Order Matters
// ✅ CORRECT: devtools wraps persist
const useStore = create<Store>()(
devtools(
persist(
(set) => ({ /* store */ }),
{ name: 'storage' }
),
{ name: 'Store' }
)
)
// Shows persist actions in DevToolsCommon Combinations
// Persist + Devtools
devtools(persist(...), { name: 'Store' })
// Persist + Immer
persist(immer(...), { name: 'storage' })
// All three
devtools(
persist(
immer(...),
{ name: 'storage' }
),
{ name: 'Store' }
)---
Custom Middleware
Logger Example
const logger = (config) => (set, get, api) => {
return config(
(...args) => {
console.log('Before:', get())
set(...args)
console.log('After:', get())
},
get,
api
)
}
const useStore = create(logger((set) => ({ /* store */ })))TypeScript Logger
import { StateCreator } from 'zustand'
type Logger = <T>(
f: StateCreator<T, [], []>,
name?: string
) => StateCreator<T, [], []>
const logger: Logger = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...a) => {
set(...(a as Parameters<typeof set>))
console.log(`[${name}]:`, get())
}
return f(loggedSet, get, store)
}---
Middleware API Reference
persist()
persist<T>(
stateCreator: StateCreator<T>,
options: {
name: string // Storage key (required)
storage?: PersistStorage<T> // Storage engine
partialize?: (state: T) => Partial<T> // Select what to persist
version?: number // Schema version
migrate?: (state: any, version: number) => T // Migration function
merge?: (persisted: any, current: T) => T // Custom merge
onRehydrateStorage?: (state: T) => void // Hydration callback
}
)devtools()
devtools<T>(
stateCreator: StateCreator<T>,
options?: {
name?: string // Store name in DevTools
enabled?: boolean // Enable/disable
}
)immer()
immer<T>(
stateCreator: StateCreator<T>
)---
Common Patterns
Reset Store
const initialState = { count: 0 }
const useStore = create<Store>()(
persist(
(set) => ({
...initialState,
reset: () => set(initialState),
}),
{ name: 'storage' }
)
)Clear Persisted Data
// Clear localStorage
localStorage.removeItem('storage-name')
// Or programmatically
const useStore = create<Store>()(
persist(
(set) => ({
clearStorage: () => {
localStorage.removeItem('storage-name')
set(initialState)
},
}),
{ name: 'storage-name' }
)
)---
Official Documentation
- Persist: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md
- Devtools: https://github.com/pmndrs/zustand/blob/main/docs/middlewares/devtools.md
- Immer: https://github.com/pmndrs/zustand/blob/main/docs/middlewares/immer.md
Migrating to Zustand
Guide for migrating from Redux, Context API, or Zustand v4. Load when user is migrating state management.
---
From Redux to Zustand
Before (Redux)
// Action types
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
// Actions
const increment = () => ({ type: INCREMENT })
const decrement = () => ({ type: DECREMENT })
// Reducer
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 }
case DECREMENT:
return { count: state.count - 1 }
default:
return state
}
}
// Store
const store = createStore(reducer)
// Provider
<Provider store={store}>
<App />
</Provider>
// Component
const count = useSelector((state) => state.count)
const dispatch = useDispatch()
<button onClick={() => dispatch(increment())}>Increment</button>After (Zustand)
// Store (all in one!)
import { create } from 'zustand'
interface Store {
count: number
increment: () => void
decrement: () => void
}
const useStore = create<Store>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// No provider needed!
// Component
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)
<button onClick={increment}>Increment</button>Benefits:
- ~90% less boilerplate
- No provider wrapper
- No action types/creators
- No reducer switch
- Built-in TypeScript support
---
From Context API to Zustand
Before (Context)
// Context
const CountContext = createContext(null)
// Provider
function CountProvider({ children }) {
const [count, setCount] = useState(0)
const increment = () => setCount((c) => c + 1)
const decrement = () => setCount((c) => c - 1)
return (
<CountContext.Provider value={{ count, increment, decrement }}>
{children}
</CountContext.Provider>
)
}
// Hook
function useCount() {
const context = useContext(CountContext)
if (!context) throw new Error('useCount must be within CountProvider')
return context
}
// App
<CountProvider>
<App />
</CountProvider>
// Component
const { count, increment } = useCount()After (Zustand)
// Store
const useStore = create<Store>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// Component (no provider needed!)
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)Benefits:
- No provider needed
- No context null checks
- No wrapper components
- Better performance (no context re-renders)
- Persistent by default (with middleware)
---
From Zustand v4 to v5
Breaking Changes
1. TypeScript Syntax
// ❌ v4
const useStore = create<Store>((set) => ({ /* ... */ }))
// ✅ v5
const useStore = create<Store>()((set) => ({ /* ... */ }))
// ^^ Double parentheses required2. Persist Middleware Imports
// ❌ v4
import { persist } from 'zustand/middleware'
const useStore = create(
persist((set) => ({ /* ... */ }), { name: 'storage' })
)
// ✅ v5
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<Store>()(
persist(
(set) => ({ /* ... */ }),
{
name: 'storage',
storage: createJSONStorage(() => localStorage),
}
)
)3. Shallow Import
// ❌ v4
import shallow from 'zustand/shallow'
// ✅ v5
import { shallow } from 'zustand/shallow'---
Migration Strategies
Gradual Migration
1. Install Zustand alongside existing solution 2. Migrate one feature at a time 3. Test thoroughly before moving to next 4. Remove old code once stable
Big Bang Migration
1. Create Zustand stores for all state 2. Update all components at once 3. Remove old state management entirely 4. Test everything
Recommendation: Gradual migration for large apps.
---
Code Mapping
Redux → Zustand
| Redux | Zustand |
|---|---|
| Actions | Direct functions in store |
| Action types | Not needed |
| Reducers | Inline in set() calls |
useSelector | Direct store selectors |
useDispatch | Direct function calls |
| Provider | Not needed |
| Middleware | Built-in (persist, devtools) |
| DevTools | devtools middleware |
Context → Zustand
| Context | Zustand |
|---|---|
createContext | create() |
| Provider | Not needed |
useContext | Direct store access |
| State | Store state |
| Updaters | Store actions |
---
Common Pitfalls
1. Forgetting Double Parentheses (v5)
// ❌ WRONG
create<Store>((set) => ({ /* ... */ }))
// ✅ CORRECT
create<Store>()((set) => ({ /* ... */ }))2. Creating Objects in Selectors
// ❌ WRONG - Causes infinite renders
const { a, b } = useStore((state) => ({ a: state.a, b: state.b }))
// ✅ CORRECT
const a = useStore((state) => state.a)
const b = useStore((state) => state.b)3. Not Using Persist Correctly
// ❌ WRONG - Missing createJSONStorage
persist((set) => ({ /* ... */ }), { name: 'storage' })
// ✅ CORRECT
persist(
(set) => ({ /* ... */ }),
{
name: 'storage',
storage: createJSONStorage(() => localStorage),
}
)---
Checklist
- [ ] Installed Zustand v5+
- [ ] Created store with
create<T>()() - [ ] Removed Context providers (if migrating from Context)
- [ ] Removed Redux boilerplate (if migrating from Redux)
- [ ] Updated all
useSelectorto Zustand selectors - [ ] Updated all
useDispatchto direct function calls - [ ] Added
persistif state needs persistence - [ ] Added
devtoolsif using Redux DevTools - [ ] Tested all components
- [ ] Verified no hydration errors (Next.js)
- [ ] Removed old state management code
---
Official Resources
- Zustand Docs: https://zustand.docs.pmnd.rs/
- Migration Guide: https://github.com/pmndrs/zustand/wiki/Migrating-to-v4
- TypeScript Guide: https://zustand.docs.pmnd.rs/guides/typescript
Zustand + Next.js Hydration Guide
Complete guide for using Zustand with Next.js SSR. Load for Next.js-specific problems.
---
The Hydration Problem
Error: "Text content does not match server-rendered HTML"
Why it happens:
- Server renders with default state
- Client loads persisted state from localStorage
- Content doesn't match → hydration error
---
Solution: Hydration Flag Pattern
Store Setup
'use client'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface Store {
_hasHydrated: boolean // Track hydration
count: number
setHasHydrated: (hydrated: boolean) => void
increment: () => void
}
export const useStore = create<Store>()(
persist(
(set) => ({
_hasHydrated: false,
count: 0,
setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{
name: 'storage',
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true) // Set true when done
},
},
),
)Component Usage
'use client'
function Component() {
const hasHydrated = useStore((state) => state._hasHydrated)
const count = useStore((state) => state.count)
if (!hasHydrated) {
return <div>Loading...</div>
}
return <div>Count: {count}</div>
}---
Alternative: Accept Flash
If loading state is unacceptable, accept brief flash:
'use client'
function Component() {
const count = useStore((state) => state.count)
// Will show default (0) briefly, then correct value
return <div>Count: {count}</div>
}Trade-off: Simpler code, but user sees flash.
---
App Router vs Pages Router
App Router (Recommended)
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
// app/page.tsx
'use client'
import { useStore } from './store'
export default function Page() {
return <Component />
}Note: Add 'use client' to components using Zustand.
Pages Router
// pages/_app.tsx
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
// pages/index.tsx
import { useStore } from '../store'
export default function Page() {
return <Component />
}Note: No 'use client' needed in Pages Router.
---
Server Components
NEVER use Zustand in Server Components:
// ❌ WRONG - Server Component
export default async function ServerComponent() {
const count = useStore((state) => state.count) // Error!
return <div>{count}</div>
}
// ✅ CORRECT - Client Component
'use client'
export default function ClientComponent() {
const count = useStore((state) => state.count)
return <div>{count}</div>
}---
SSR with getServerSideProps
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
// Fetch data on server
const data = await fetchData()
return {
props: { data },
}
}
export default function Page({ data }) {
const setData = useStore((state) => state.setData)
// Sync server data to store
useEffect(() => {
setData(data)
}, [data, setData])
return <Component />
}---
Common Patterns
Initialize Store from Props
'use client'
function Page({ serverData }) {
const setData = useStore((state) => state.setData)
useEffect(() => {
setData(serverData)
}, [serverData, setData])
return <Component />
}Conditional Rendering
'use client'
function Component() {
const hasHydrated = useStore((state) => state._hasHydrated)
const theme = useStore((state) => state.theme)
return (
<div className={hasHydrated ? theme : 'default'}>
{hasHydrated ? <Content /> : <Skeleton />}
</div>
)
}Progressive Enhancement
'use client'
function Component() {
const hasHydrated = useStore((state) => state._hasHydrated)
const preferences = useStore((state) => state.preferences)
// Always render, but enhance after hydration
return (
<div>
<DefaultUI />
{hasHydrated && <EnhancedUI preferences={preferences} />}
</div>
)
}---
Debugging Hydration Errors
Enable Strict Mode
// next.config.js
module.exports = {
reactStrictMode: true,
}Check for Differences
useEffect(() => {
console.log('Server state:', defaultState)
console.log('Client state:', persistedState)
}, [])Use React DevTools
Look for red warnings about hydration mismatches.
---
Official Resources
- Next.js Hydration: https://nextjs.org/docs/messages/react-hydration-error
- Zustand Persist: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md
- DEV.to Guide: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5
Zustand TypeScript Advanced Patterns
Advanced TypeScript patterns and troubleshooting. Load when encountering complex type inference issues.
---
Basic TypeScript Setup
The Golden Rule: Double Parentheses
// ✅ CORRECT
const useStore = create<Store>()((set) => ({ /* ... */ }))
// ❌ WRONG
const useStore = create<Store>((set) => ({ /* ... */ }))Why: Currying syntax enables middleware type inference.
---
Store Interface Pattern
// Define types
interface BearState {
bears: number
}
interface BearActions {
increase: (by: number) => void
decrease: (by: number) => void
}
// Combine
type BearStore = BearState & BearActions
// Use
const useBearStore = create<BearStore>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
decrease: (by) => set((state) => ({ bears: state.bears - by })),
}))---
Slices Pattern Types
import { StateCreator } from 'zustand'
// Define slice type
interface BearSlice {
bears: number
addBear: () => void
}
// Create slice with proper types
const createBearSlice: StateCreator<
BearSlice & FishSlice, // Combined store type
[], // Middleware mutators
[], // Chained middleware
BearSlice // This slice
> = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
})---
Middleware Types
With Devtools
import { StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
interface Store {
count: number
increment: () => void
}
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
increment: () =>
set(
(state) => ({ count: state.count + 1 }),
undefined,
'increment'
),
}),
{ name: 'Store' }
)
)With Persist
import { persist } from 'zustand/middleware'
const useStore = create<Store>()(
persist(
(set) => ({ /* ... */ }),
{ name: 'storage' }
)
)With Multiple Middlewares
const useStore = create<Store>()(
devtools(
persist(
(set) => ({ /* ... */ }),
{ name: 'storage' }
),
{ name: 'Store' }
)
)---
Slices with Middleware
import { StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
const createBearSlice: StateCreator<
BearSlice & FishSlice,
[['zustand/devtools', never]], // Add devtools mutator
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () =>
set(
(state) => ({ bears: state.bears + 1 }),
undefined,
'bear/add'
),
})---
Selector Types
Basic Selector
const bears = useStore((state) => state.bears)
// Type: numberComputed Selector
const selectTotal = (state: Store) => state.items.reduce((sum, item) => sum + item.price, 0)
const total = useStore(selectTotal)
// Type: numberParameterized Selector
const selectById = (id: string) => (state: Store) =>
state.items.find((item) => item.id === id)
const item = useStore(selectById('123'))
// Type: Item | undefined---
Vanilla Store Types
import { createStore } from 'zustand/vanilla'
const store = createStore<Store>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
// Get state
const state = store.getState()
// Type: Store
// Subscribe
const unsubscribe = store.subscribe((state) => {
// state is typed as Store
})---
Custom Hook with Types
import { useStore } from 'zustand'
const bearStore = createStore<BearStore>()((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}))
// Create custom hook
function useBearStore<T>(selector: (state: BearStore) => T): T {
return useStore(bearStore, selector)
}---
Common Type Errors
Error: Type inference breaks
Problem: Using single parentheses
// ❌ WRONG
const useStore = create<Store>((set) => ({ /* ... */ }))Solution: Use double parentheses
// ✅ CORRECT
const useStore = create<Store>()((set) => ({ /* ... */ }))Error: StateCreator types fail
Problem: Missing middleware mutators
// ❌ WRONG
const createSlice: StateCreator<CombinedStore, [], [], MySlice>Solution: Add middleware mutators
// ✅ CORRECT
const createSlice: StateCreator<
CombinedStore,
[['zustand/devtools', never]], // Add this
[],
MySlice
>Error: Circular reference
Problem: Slices referencing each other
Solution: Define combined type first, then reference in slices
type AllSlices = BearSlice & FishSlice & SharedSlice
const createBearSlice: StateCreator<AllSlices, [], [], BearSlice> = ...---
Official TypeScript Guide
https://zustand.docs.pmnd.rs/guides/typescript
#!/bin/bash
# Zustand Version Checker
# Verifies installed versions and compatibility
# Usage: ./scripts/check-versions.sh
set -e
echo "🐻 Zustand Version Checker"
echo "=========================="
echo ""
# Check if package.json exists
if [ ! -f "package.json" ]; then
echo "❌ No package.json found in current directory"
echo " Run this script from your project root"
exit 1
fi
# Function to get installed version
get_version() {
local package=$1
if [ -f "node_modules/$package/package.json" ]; then
node -p "require('./node_modules/$package/package.json').version" 2>/dev/null || echo "not installed"
else
echo "not installed"
fi
}
# Function to get latest version from npm
get_latest() {
local package=$1
npm view "$package" version 2>/dev/null || echo "unknown"
}
# Check Zustand
echo "📦 Zustand"
zustand_installed=$(get_version "zustand")
zustand_latest=$(get_latest "zustand")
if [ "$zustand_installed" = "not installed" ]; then
echo " Status: ❌ Not installed"
echo " Install: npm install zustand"
else
echo " Installed: $zustand_installed"
echo " Latest: $zustand_latest"
# Check if major version is 5
major=$(echo "$zustand_installed" | cut -d'.' -f1)
if [ "$major" -ge 5 ]; then
echo " Status: ✅ v5+ (recommended)"
elif [ "$major" -eq 4 ]; then
echo " Status: ⚠️ v4 (upgrade recommended)"
echo " Migration: See references/migration-guide.md"
else
echo " Status: ❌ v$major (unsupported)"
echo " Action: Upgrade to v5+"
fi
fi
echo ""
# Check React
echo "📦 React"
react_installed=$(get_version "react")
react_latest=$(get_latest "react")
if [ "$react_installed" = "not installed" ]; then
echo " Status: ❌ Not installed"
else
echo " Installed: $react_installed"
echo " Latest: $react_latest"
# Check if version is 18+
major=$(echo "$react_installed" | cut -d'.' -f1)
if [ "$major" -ge 18 ]; then
echo " Status: ✅ v18+ (compatible)"
else
echo " Status: ⚠️ v$major (Zustand works, but upgrade recommended)"
fi
fi
echo ""
# Check TypeScript (optional)
echo "📦 TypeScript (optional)"
ts_installed=$(get_version "typescript")
if [ "$ts_installed" = "not installed" ]; then
echo " Status: ℹ️ Not installed (optional)"
else
echo " Installed: $ts_installed"
# Check if version is 5+
major=$(echo "$ts_installed" | cut -d'.' -f1)
if [ "$major" -ge 5 ]; then
echo " Status: ✅ v5+ (recommended)"
else
echo " Status: ⚠️ v$major (upgrade recommended for better types)"
fi
fi
echo ""
# Check Next.js (if using persist)
if grep -q '"next"' package.json 2>/dev/null; then
echo "📦 Next.js (detected)"
next_installed=$(get_version "next")
echo " Installed: $next_installed"
# Check if major version is 14+
major=$(echo "$next_installed" | cut -d'.' -f1)
if [ "$major" -ge 14 ]; then
echo " Status: ✅ v14+ (App Router supported)"
elif [ "$major" -eq 13 ]; then
echo " Status: ✅ v13 (supported)"
else
echo " Status: ⚠️ v$major (upgrade recommended)"
fi
echo " Note: See references/nextjs-hydration.md for SSR setup"
echo ""
fi
# Summary
echo "=========================="
echo "✅ Version check complete"
echo ""
if [ "$zustand_installed" = "not installed" ]; then
echo "Next steps:"
echo " npm install zustand"
echo " See SKILL.md for setup instructions"
elif [ "$major" -lt 5 ]; then
echo "Recommended upgrade:"
echo " npm install zustand@latest"
echo " See references/migration-guide.md for v4→v5 changes"
else
echo "All versions compatible! 🎉"
echo "See SKILL.md for usage patterns"
fi
#!/bin/bash
# [TODO: Script Name]
# [TODO: Brief description of what this script does]
# Example script structure - delete if not needed
set -e # Exit on error
# [TODO: Add your script logic here]
echo "Example script - replace or delete this file"
# Usage:
# ./scripts/example-script.sh [args]
/**
* Zustand Store with Async Actions
*
* Use when:
* - Fetching data from APIs
* - Need loading/error states
* - Handling async operations
*
* Pattern: Actions can be async, just call set() when done
*
* Features:
* - Loading states
* - Error handling
* - Optimistic updates
* - Request cancellation
*
* NOTE: For server state (data fetching), consider TanStack Query instead!
* Zustand is better for client state. But this pattern works for simple cases.
*
* Learn more: See SKILL.md Common Patterns section
*/
import { create } from 'zustand'
interface User {
id: string
name: string
email: string
}
interface Post {
id: string
title: string
body: string
userId: string
}
interface AsyncStore {
// Data state
user: User | null
posts: Post[]
// Loading states
isLoadingUser: boolean
isLoadingPosts: boolean
isSavingPost: boolean
// Error states
userError: string | null
postsError: string | null
saveError: string | null
// Actions
fetchUser: (userId: string) => Promise<void>
fetchPosts: (userId: string) => Promise<void>
createPost: (post: Omit<Post, 'id'>) => Promise<void>
deletePost: (postId: string) => Promise<void>
reset: () => void
}
export const useAsyncStore = create<AsyncStore>()((set, get) => ({
// Initial state
user: null,
posts: [],
isLoadingUser: false,
isLoadingPosts: false,
isSavingPost: false,
userError: null,
postsError: null,
saveError: null,
// Fetch user
fetchUser: async (userId) => {
set({ isLoadingUser: true, userError: null })
try {
const response = await fetch(`https://api.example.com/users/${userId}`)
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`)
}
const user = await response.json()
set({ user, isLoadingUser: false })
} catch (error) {
set({
userError: (error as Error).message,
isLoadingUser: false,
user: null,
})
}
},
// Fetch posts
fetchPosts: async (userId) => {
set({ isLoadingPosts: true, postsError: null })
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`)
if (!response.ok) {
throw new Error(`Failed to fetch posts: ${response.statusText}`)
}
const posts = await response.json()
set({ posts, isLoadingPosts: false })
} catch (error) {
set({
postsError: (error as Error).message,
isLoadingPosts: false,
})
}
},
// Create post with optimistic update
createPost: async (post) => {
const tempId = `temp-${Date.now()}`
const optimisticPost = { ...post, id: tempId }
// Optimistic update
set((state) => ({
posts: [...state.posts, optimisticPost],
isSavingPost: true,
saveError: null,
}))
try {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
})
if (!response.ok) {
throw new Error(`Failed to create post: ${response.statusText}`)
}
const savedPost = await response.json()
// Replace optimistic post with real one
set((state) => ({
posts: state.posts.map((p) =>
p.id === tempId ? savedPost : p
),
isSavingPost: false,
}))
} catch (error) {
// Rollback optimistic update
set((state) => ({
posts: state.posts.filter((p) => p.id !== tempId),
saveError: (error as Error).message,
isSavingPost: false,
}))
}
},
// Delete post
deletePost: async (postId) => {
// Store original posts for rollback
const originalPosts = get().posts
// Optimistic update
set((state) => ({
posts: state.posts.filter((p) => p.id !== postId),
}))
try {
const response = await fetch(`https://api.example.com/posts/${postId}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error(`Failed to delete post: ${response.statusText}`)
}
} catch (error) {
// Rollback on error
set({
posts: originalPosts,
saveError: (error as Error).message,
})
}
},
// Reset
reset: () =>
set({
user: null,
posts: [],
isLoadingUser: false,
isLoadingPosts: false,
isSavingPost: false,
userError: null,
postsError: null,
saveError: null,
}),
}))
/**
* Usage in components:
*
* function UserProfile({ userId }: { userId: string }) {
* const user = useAsyncStore((state) => state.user)
* const isLoading = useAsyncStore((state) => state.isLoadingUser)
* const error = useAsyncStore((state) => state.userError)
* const fetchUser = useAsyncStore((state) => state.fetchUser)
*
* useEffect(() => {
* fetchUser(userId)
* }, [userId, fetchUser])
*
* if (isLoading) return <div>Loading...</div>
* if (error) return <div>Error: {error}</div>
* if (!user) return <div>No user found</div>
*
* return (
* <div>
* <h1>{user.name}</h1>
* <p>{user.email}</p>
* </div>
* )
* }
*
* function PostsList({ userId }: { userId: string }) {
* const posts = useAsyncStore((state) => state.posts)
* const isLoading = useAsyncStore((state) => state.isLoadingPosts)
* const error = useAsyncStore((state) => state.postsError)
* const fetchPosts = useAsyncStore((state) => state.fetchPosts)
* const deletePost = useAsyncStore((state) => state.deletePost)
*
* useEffect(() => {
* fetchPosts(userId)
* }, [userId, fetchPosts])
*
* if (isLoading) return <div>Loading posts...</div>
* if (error) return <div>Error: {error}</div>
*
* return (
* <ul>
* {posts.map((post) => (
* <li key={post.id}>
* <h3>{post.title}</h3>
* <p>{post.body}</p>
* <button onClick={() => deletePost(post.id)}>Delete</button>
* </li>
* ))}
* </ul>
* )
* }
*
* function CreatePostForm({ userId }: { userId: string }) {
* const [title, setTitle] = useState('')
* const [body, setBody] = useState('')
* const createPost = useAsyncStore((state) => state.createPost)
* const isSaving = useAsyncStore((state) => state.isSavingPost)
* const error = useAsyncStore((state) => state.saveError)
*
* const handleSubmit = async (e: FormEvent) => {
* e.preventDefault()
* await createPost({ title, body, userId })
* setTitle('')
* setBody('')
* }
*
* return (
* <form onSubmit={handleSubmit}>
* <input
* value={title}
* onChange={(e) => setTitle(e.target.value)}
* placeholder="Title"
* />
* <textarea
* value={body}
* onChange={(e) => setBody(e.target.value)}
* placeholder="Body"
* />
* <button type="submit" disabled={isSaving}>
* {isSaving ? 'Creating...' : 'Create Post'}
* </button>
* {error && <div>Error: {error}</div>}
* </form>
* )
* }
*
* ADVANCED: Request Cancellation with AbortController
*
* let abortController: AbortController | null = null
*
* fetchUserWithCancellation: async (userId) => {
* // Cancel previous request
* abortController?.abort()
* abortController = new AbortController()
*
* set({ isLoadingUser: true, userError: null })
*
* try {
* const response = await fetch(
* `https://api.example.com/users/${userId}`,
* { signal: abortController.signal }
* )
* // ... rest of fetch logic
* } catch (error) {
* if (error.name === 'AbortError') {
* // Request was cancelled
* return
* }
* // ... handle other errors
* }
* }
*/
/**
* Basic Zustand Store (JavaScript style, no TypeScript)
*
* Use when:
* - Prototyping quickly
* - Small applications
* - No TypeScript in project
*
* Learn more: See SKILL.md for TypeScript version
*/
import { create } from 'zustand'
// Create store with minimal setup
export const useStore = create((set) => ({
// State
count: 0,
user: null,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null }),
}))
/**
* Usage in component:
*
* function Counter() {
* const count = useStore((state) => state.count)
* const increment = useStore((state) => state.increment)
*
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*/
/**
* Zustand Store with Computed/Derived Values
*
* Use when:
* - Need values calculated from state
* - Want to avoid storing redundant data
* - Need efficient memoization
*
* Pattern: Compute in selectors, not in store
*
* Benefits:
* - Cleaner state (only source data)
* - Automatic recomputation when dependencies change
* - Components only re-render when result changes
*
* Learn more: See SKILL.md Common Patterns section
*/
import { create } from 'zustand'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface CartItem {
productId: string
quantity: number
}
interface ComputedStore {
// Source state (stored)
products: Product[]
cart: CartItem[]
taxRate: number
discountPercent: number
// Actions
addProduct: (product: Product) => void
addToCart: (productId: string, quantity: number) => void
removeFromCart: (productId: string) => void
setTaxRate: (rate: number) => void
setDiscount: (percent: number) => void
}
export const useComputedStore = create<ComputedStore>()((set) => ({
// Initial state
products: [],
cart: [],
taxRate: 0.1, // 10%
discountPercent: 0,
// Actions
addProduct: (product) =>
set((state) => ({
products: [...state.products, product],
})),
addToCart: (productId, quantity) =>
set((state) => {
const existing = state.cart.find((item) => item.productId === productId)
if (existing) {
return {
cart: state.cart.map((item) =>
item.productId === productId
? { ...item, quantity: item.quantity + quantity }
: item
),
}
}
return {
cart: [...state.cart, { productId, quantity }],
}
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((item) => item.productId !== productId),
})),
setTaxRate: (rate) => set({ taxRate: rate }),
setDiscount: (percent) => set({ discountPercent: percent }),
}))
// ============================================================================
// COMPUTED/DERIVED SELECTORS (put these in separate file for reuse)
// ============================================================================
/**
* Selector: Get cart items with product details
*/
export const selectCartWithDetails = (state: ComputedStore) =>
state.cart.map((item) => {
const product = state.products.find((p) => p.id === item.productId)
return {
...item,
product,
subtotal: product ? product.price * item.quantity : 0,
}
})
/**
* Selector: Calculate subtotal (before tax and discount)
*/
export const selectSubtotal = (state: ComputedStore) =>
state.cart.reduce((sum, item) => {
const product = state.products.find((p) => p.id === item.productId)
return sum + (product ? product.price * item.quantity : 0)
}, 0)
/**
* Selector: Calculate discount amount
*/
export const selectDiscountAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
return subtotal * (state.discountPercent / 100)
}
/**
* Selector: Calculate tax amount
*/
export const selectTaxAmount = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const afterDiscount = subtotal - discountAmount
return afterDiscount * state.taxRate
}
/**
* Selector: Calculate final total
*/
export const selectTotal = (state: ComputedStore) => {
const subtotal = selectSubtotal(state)
const discountAmount = selectDiscountAmount(state)
const taxAmount = selectTaxAmount(state)
return subtotal - discountAmount + taxAmount
}
/**
* Selector: Get products by category
*/
export const selectProductsByCategory = (category: string) => (state: ComputedStore) =>
state.products.filter((product) => product.category === category)
/**
* Selector: Get in-stock products only
*/
export const selectInStockProducts = (state: ComputedStore) =>
state.products.filter((product) => product.inStock)
/**
* Selector: Count items in cart
*/
export const selectCartItemCount = (state: ComputedStore) =>
state.cart.reduce((sum, item) => sum + item.quantity, 0)
/**
* Usage in components:
*
* function CartSummary() {
* // Each selector only causes re-render when its result changes
* const subtotal = useComputedStore(selectSubtotal)
* const discount = useComputedStore(selectDiscountAmount)
* const tax = useComputedStore(selectTaxAmount)
* const total = useComputedStore(selectTotal)
* const itemCount = useComputedStore(selectCartItemCount)
*
* return (
* <div>
* <h3>Cart ({itemCount} items)</h3>
* <p>Subtotal: ${subtotal.toFixed(2)}</p>
* <p>Discount: -${discount.toFixed(2)}</p>
* <p>Tax: ${tax.toFixed(2)}</p>
* <h4>Total: ${total.toFixed(2)}</h4>
* </div>
* )
* }
*
* function ProductList() {
* const inStockProducts = useComputedStore(selectInStockProducts)
*
* return (
* <ul>
* {inStockProducts.map((product) => (
* <li key={product.id}>{product.name} - ${product.price}</li>
* ))}
* </ul>
* )
* }
*
* function CategoryFilter() {
* const electronicsProducts = useComputedStore(selectProductsByCategory('electronics'))
*
* return <div>{electronicsProducts.length} electronics</div>
* }
*
* PERFORMANCE TIP:
* For expensive computations, use useMemo in the component:
*
* function ExpensiveComputation() {
* const data = useComputedStore((state) => state.products)
*
* const result = useMemo(() => {
* // Expensive calculation
* return data.map(...).filter(...).reduce(...)
* }, [data])
*
* return <div>{result}</div>
* }
*/
/**
* Zustand Store with Redux DevTools Integration
*
* Use when:
* - Need debugging capabilities
* - Want to track state changes
* - Inspecting actions and state history
* - Time-travel debugging (with Redux DevTools Extension)
*
* Setup:
* 1. Install Redux DevTools Extension in browser
* 2. Use this template
* 3. Open DevTools → Redux tab
*
* Learn more: See SKILL.md for combining with other middleware
*/
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface Todo {
id: string
text: string
done: boolean
}
interface DevtoolsStore {
// State
todos: Todo[]
filter: 'all' | 'active' | 'completed'
// Actions
addTodo: (text: string) => void
toggleTodo: (id: string) => void
deleteTodo: (id: string) => void
setFilter: (filter: DevtoolsStore['filter']) => void
clearCompleted: () => void
}
export const useDevtoolsStore = create<DevtoolsStore>()(
devtools(
(set) => ({
// Initial state
todos: [],
filter: 'all',
// Actions with named actions for DevTools
addTodo: (text) =>
set(
(state) => ({
todos: [
...state.todos,
{ id: Date.now().toString(), text, done: false },
],
}),
undefined,
'todos/add', // Action name in DevTools
),
toggleTodo: (id) =>
set(
(state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
),
}),
undefined,
'todos/toggle',
),
deleteTodo: (id) =>
set(
(state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
}),
undefined,
'todos/delete',
),
setFilter: (filter) =>
set(
{ filter },
undefined,
'filter/set',
),
clearCompleted: () =>
set(
(state) => ({
todos: state.todos.filter((todo) => !todo.done),
}),
undefined,
'todos/clearCompleted',
),
}),
{
name: 'TodoStore', // Store name in DevTools
enabled: process.env.NODE_ENV === 'development', // Optional: only in dev
},
),
)
/**
* Usage in component:
*
* function TodoList() {
* const todos = useDevtoolsStore((state) => state.todos)
* const filter = useDevtoolsStore((state) => state.filter)
* const toggleTodo = useDevtoolsStore((state) => state.toggleTodo)
* const deleteTodo = useDevtoolsStore((state) => state.deleteTodo)
*
* const filteredTodos = todos.filter((todo) => {
* if (filter === 'active') return !todo.done
* if (filter === 'completed') return todo.done
* return true
* })
*
* return (
* <ul>
* {filteredTodos.map((todo) => (
* <li key={todo.id}>
* <input
* type="checkbox"
* checked={todo.done}
* onChange={() => toggleTodo(todo.id)}
* />
* <span>{todo.text}</span>
* <button onClick={() => deleteTodo(todo.id)}>Delete</button>
* </li>
* ))}
* </ul>
* )
* }
*
* DevTools Features:
* - See all actions in order
* - Inspect state before/after each action
* - Time-travel through state changes
* - Export/import state for debugging
* - Track which components caused updates
*/
/**
* Next.js SSR-Safe Zustand Store with Hydration Handling
*
* Use when:
* - Using persist middleware in Next.js
* - Getting hydration mismatch errors
* - Need SSR compatibility
*
* This template solves:
* - "Text content does not match server-rendered HTML"
* - "Hydration failed" errors
* - Flashing content during hydration
*
* Learn more: See SKILL.md Issue #1 for detailed explanation
*/
'use client' // Required for Next.js App Router
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface User {
id: string
name: string
}
interface NextJsStore {
// State
_hasHydrated: boolean // CRITICAL: Track hydration status
count: number
user: User | null
// Actions
setHasHydrated: (hydrated: boolean) => void
increment: () => void
setUser: (user: User | null) => void
reset: () => void
}
export const useNextJsStore = create<NextJsStore>()(
persist(
(set) => ({
// Initial state
_hasHydrated: false, // Start false
count: 0,
user: null,
// Hydration action
setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
// Regular actions
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),
reset: () => set({ count: 0, user: null }),
}),
{
name: 'nextjs-storage',
// CRITICAL: Call setHasHydrated when rehydration completes
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true)
},
},
),
)
/**
* Usage in Next.js component:
*
* 'use client'
*
* import { useNextJsStore } from './store'
*
* function Counter() {
* const hasHydrated = useNextJsStore((state) => state._hasHydrated)
* const count = useNextJsStore((state) => state.count)
* const increment = useNextJsStore((state) => state.increment)
*
* // Show loading state during hydration
* if (!hasHydrated) {
* return <div>Loading...</div>
* }
*
* // Now safe to render with persisted state
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*
* Alternative: Skip hydration check if flashing is acceptable
*
* function CounterNoLoadingState() {
* const count = useNextJsStore((state) => state.count)
* const increment = useNextJsStore((state) => state.increment)
*
* // Will show default value (0) until hydration, then correct value
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*/
/**
* Persistent Zustand Store (localStorage/sessionStorage)
*
* Use when:
* - State needs to survive page reloads
* - User preferences (theme, language, etc.)
* - Shopping carts, draft forms
*
* Features:
* - Automatic save to localStorage
* - Automatic restore on load
* - Migration support for schema changes
* - Partial persistence (only save some fields)
*
* Learn more: See SKILL.md Issue #1 for Next.js hydration handling
*/
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface User {
id: string
name: string
email: string
}
interface PersistedStore {
// State
theme: 'light' | 'dark' | 'system'
language: string
user: User | null
preferences: {
notifications: boolean
emailUpdates: boolean
}
// Actions
setTheme: (theme: PersistedStore['theme']) => void
setLanguage: (language: string) => void
setUser: (user: User | null) => void
updatePreferences: (prefs: Partial<PersistedStore['preferences']>) => void
reset: () => void
}
const initialState = {
theme: 'system' as const,
language: 'en',
user: null,
preferences: {
notifications: true,
emailUpdates: false,
},
}
export const usePersistedStore = create<PersistedStore>()(
persist(
(set) => ({
...initialState,
// Actions
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
setUser: (user) => set({ user }),
updatePreferences: (prefs) =>
set((state) => ({
preferences: { ...state.preferences, ...prefs },
})),
reset: () => set(initialState),
}),
{
name: 'app-storage', // unique name in localStorage
// Optional: use sessionStorage instead
// storage: createJSONStorage(() => sessionStorage),
// Optional: only persist specific fields
// partialize: (state) => ({
// theme: state.theme,
// language: state.language,
// preferences: state.preferences,
// // Don't persist user (privacy)
// }),
// Optional: version and migration for schema changes
version: 1,
migrate: (persistedState: any, version) => {
if (version === 0) {
// Migration from version 0 to 1
// Example: rename field
persistedState.language = persistedState.lang || 'en'
delete persistedState.lang
}
return persistedState
},
},
),
)
/**
* Usage in component:
*
* function ThemeToggle() {
* const theme = usePersistedStore((state) => state.theme)
* const setTheme = usePersistedStore((state) => state.setTheme)
*
* return (
* <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
* Toggle theme
* </button>
* )
* }
*
* NEXT.JS WARNING:
* For Next.js with SSR, see nextjs-store.ts template for hydration handling!
*/
/**
* Zustand Slices Pattern (Modular Store Architecture)
*
* Use when:
* - Store is getting large (100+ lines)
* - Multiple developers working on store
* - Need to organize state by domain/feature
*
* Benefits:
* - Each slice is independent and testable
* - Slices can access each other's state/actions
* - Easy to add/remove features
* - Better code organization
*
* Learn more: See SKILL.md Issue #5 for TypeScript complexity handling
*/
import { create, StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
// ============================================================================
// SLICE 1: User Management
// ============================================================================
interface User {
id: string
name: string
email: string
}
interface UserSlice {
user: User | null
isAuthenticated: boolean
setUser: (user: User) => void
logout: () => void
}
const createUserSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice, // Combined type
[['zustand/devtools', never]], // Middleware mutators
[], // Chained middleware
UserSlice // This slice's type
> = (set) => ({
user: null,
isAuthenticated: false,
setUser: (user) =>
set(
{ user, isAuthenticated: true },
undefined,
'user/setUser',
),
logout: () =>
set(
{ user: null, isAuthenticated: false },
undefined,
'user/logout',
),
})
// ============================================================================
// SLICE 2: Shopping Cart
// ============================================================================
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartSlice {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
clearCart: () => void
// Access other slices
eatFish: () => void // Example: cross-slice action
}
const createCartSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice,
[['zustand/devtools', never]],
[],
CartSlice
> = (set, get) => ({
items: [],
addItem: (item) =>
set(
(state) => ({
items: [...state.items, item],
}),
undefined,
'cart/addItem',
),
removeItem: (id) =>
set(
(state) => ({
items: state.items.filter((item) => item.id !== id),
}),
undefined,
'cart/removeItem',
),
clearCart: () =>
set(
{ items: [] },
undefined,
'cart/clearCart',
),
// Example: Action that accesses another slice
eatFish: () =>
set(
(state) => ({
// Access notification slice
notifications: state.notifications + 1,
}),
undefined,
'cart/eatFish',
),
})
// ============================================================================
// SLICE 3: Notifications
// ============================================================================
interface NotificationSlice {
notifications: number
messages: string[]
addNotification: (message: string) => void
clearNotifications: () => void
}
const createNotificationSlice: StateCreator<
UserSlice & CartSlice & NotificationSlice,
[['zustand/devtools', never]],
[],
NotificationSlice
> = (set) => ({
notifications: 0,
messages: [],
addNotification: (message) =>
set(
(state) => ({
notifications: state.notifications + 1,
messages: [...state.messages, message],
}),
undefined,
'notifications/add',
),
clearNotifications: () =>
set(
{ notifications: 0, messages: [] },
undefined,
'notifications/clear',
),
})
// ============================================================================
// COMBINE SLICES
// ============================================================================
export const useAppStore = create<UserSlice & CartSlice & NotificationSlice>()(
devtools(
(...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
...createNotificationSlice(...a),
}),
{ name: 'AppStore' },
),
)
/**
* Usage in components:
*
* function UserProfile() {
* const user = useAppStore((state) => state.user)
* const logout = useAppStore((state) => state.logout)
*
* if (!user) return <div>Not logged in</div>
*
* return (
* <div>
* <p>Welcome, {user.name}!</p>
* <button onClick={logout}>Logout</button>
* </div>
* )
* }
*
* function Cart() {
* const items = useAppStore((state) => state.items)
* const clearCart = useAppStore((state) => state.clearCart)
*
* return (
* <div>
* <h2>Cart ({items.length} items)</h2>
* <button onClick={clearCart}>Clear</button>
* </div>
* )
* }
*
* ORGANIZATION TIP:
* In larger projects, put each slice in its own file:
*
* src/store/
* ├── index.ts (combines slices)
* ├── userSlice.ts
* ├── cartSlice.ts
* └── notificationSlice.ts
*/
/**
* TypeScript Zustand Store (Recommended for production)
*
* Use when:
* - Production applications
* - Need type safety
* - Want IDE autocomplete
*
* CRITICAL: Notice the double parentheses `create<T>()()` - required for TypeScript
*
* Learn more: See SKILL.md for middleware and advanced patterns
*/
import { create } from 'zustand'
// Define state interface
interface User {
id: string
name: string
email: string
}
// Define store interface (state + actions)
interface AppStore {
// State
count: number
user: User | null
isLoading: boolean
// Actions
increment: () => void
decrement: () => void
reset: () => void
setUser: (user: User) => void
clearUser: () => void
setLoading: (loading: boolean) => void
}
// Create typed store with double parentheses
export const useAppStore = create<AppStore>()((set) => ({
// Initial state
count: 0,
user: null,
isLoading: false,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0, user: null }),
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
setLoading: (loading) => set({ isLoading: loading }),
}))
/**
* Usage in component:
*
* function Counter() {
* // Select single value (component only re-renders when count changes)
* const count = useAppStore((state) => state.count)
* const increment = useAppStore((state) => state.increment)
*
* return (
* <div>
* <p>Count: {count}</p>
* <button onClick={increment}>Increment</button>
* </div>
* )
* }
*
* function UserProfile() {
* const user = useAppStore((state) => state.user)
* const setUser = useAppStore((state) => state.setUser)
*
* if (!user) return <div>No user</div>
*
* return <div>Hello, {user.name}!</div>
* }
*/