Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "zustand-state-management",
|
||||||
|
"description": "Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Jeremy Dawes",
|
||||||
|
"email": "jeremy@jezweb.net"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# zustand-state-management
|
||||||
|
|
||||||
|
Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.
|
||||||
799
SKILL.md
Normal file
799
SKILL.md
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
---
|
||||||
|
name: zustand-state-management
|
||||||
|
description: |
|
||||||
|
Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR.
|
||||||
|
|
||||||
|
Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.
|
||||||
|
license: MIT
|
||||||
|
---
|
||||||
|
|
||||||
|
# Zustand State Management
|
||||||
|
|
||||||
|
**Status**: Production Ready ✅
|
||||||
|
**Last Updated**: 2025-10-24
|
||||||
|
**Latest Version**: zustand@5.0.8
|
||||||
|
**Dependencies**: React 18+, TypeScript 5+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start (3 Minutes)
|
||||||
|
|
||||||
|
### 1. Install Zustand
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install zustand
|
||||||
|
# or
|
||||||
|
pnpm add zustand
|
||||||
|
# or
|
||||||
|
yarn add zustand
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why 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)
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
This skill prevents **5** documented issues:
|
||||||
|
|
||||||
|
### 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](https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5)
|
||||||
|
- GitHub Discussions #2839
|
||||||
|
|
||||||
|
**Why It Happens**:
|
||||||
|
Persist middleware reads from localStorage on client but not on server, causing state mismatch.
|
||||||
|
|
||||||
|
**Prevention**:
|
||||||
|
```typescript
|
||||||
|
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](https://zustand.docs.pmnd.rs/guides/typescript)
|
||||||
|
|
||||||
|
**Why It Happens**:
|
||||||
|
The currying syntax `create<T>()()` is required for middleware to work with TypeScript inference.
|
||||||
|
|
||||||
|
**Prevention**:
|
||||||
|
```typescript
|
||||||
|
// ❌ 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**:
|
||||||
|
```typescript
|
||||||
|
// ✅ 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**:
|
||||||
|
```typescript
|
||||||
|
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](https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md)
|
||||||
|
|
||||||
|
**Why It Happens**:
|
||||||
|
Combining multiple slices requires explicit type annotations for middleware compatibility.
|
||||||
|
|
||||||
|
**Prevention**:
|
||||||
|
```typescript
|
||||||
|
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),
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Middleware Configuration
|
||||||
|
|
||||||
|
### Persist Middleware (localStorage)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface MyStore {
|
||||||
|
data: string[]
|
||||||
|
addItem: (item: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStore = create<MyStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
data: [],
|
||||||
|
addItem: (item) => set((state) => ({ data: [...state.data, item] })),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'my-storage',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
partialize: (state) => ({ data: state.data }), // Only persist 'data'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Devtools Middleware (Redux DevTools)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
interface CounterStore {
|
||||||
|
count: number
|
||||||
|
increment: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStore = create<CounterStore>()(
|
||||||
|
devtools(
|
||||||
|
(set) => ({
|
||||||
|
count: 0,
|
||||||
|
increment: () =>
|
||||||
|
set(
|
||||||
|
(state) => ({ count: state.count + 1 }),
|
||||||
|
undefined,
|
||||||
|
'counter/increment', // Action name in DevTools
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{ name: 'CounterStore' }, // Store name in DevTools
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining Multiple Middlewares
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { devtools, persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
const useStore = create<MyStore>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
// store definition
|
||||||
|
}),
|
||||||
|
{ name: 'my-storage' },
|
||||||
|
),
|
||||||
|
{ name: 'MyStore' },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Order matters**: `devtools(persist(...))` shows persist actions in DevTools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: Computed/Derived Values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Async Actions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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')
|
||||||
|
const data = await response.text()
|
||||||
|
set({ data, isLoading: false })
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: (error as Error).message, isLoading: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Resetting Store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ResettableStore {
|
||||||
|
count: number
|
||||||
|
name: string
|
||||||
|
increment: () => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
count: 0,
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStore = create<ResettableStore>()((set) => ({
|
||||||
|
...initialState,
|
||||||
|
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||||
|
reset: () => set(initialState),
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Selector with Params
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using Bundled Resources
|
||||||
|
|
||||||
|
### Templates (templates/)
|
||||||
|
|
||||||
|
This skill includes 8 ready-to-use template files:
|
||||||
|
|
||||||
|
- `basic-store.ts` - Minimal JavaScript store example
|
||||||
|
- `typescript-store.ts` - Properly typed TypeScript store
|
||||||
|
- `persist-store.ts` - localStorage persistence with migration
|
||||||
|
- `slices-pattern.ts` - Modular store organization
|
||||||
|
- `devtools-store.ts` - Redux DevTools integration
|
||||||
|
- `nextjs-store.ts` - SSR-safe Next.js store with hydration
|
||||||
|
- `computed-store.ts` - Derived state patterns
|
||||||
|
- `async-actions-store.ts` - Async operations with loading states
|
||||||
|
|
||||||
|
**Example Usage:**
|
||||||
|
```bash
|
||||||
|
# Copy template to your project
|
||||||
|
cp ~/.claude/skills/zustand-state-management/templates/typescript-store.ts src/store/
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use each:**
|
||||||
|
- Use `basic-store.ts` for quick prototypes
|
||||||
|
- Use `typescript-store.ts` for most production apps
|
||||||
|
- Use `persist-store.ts` when state needs to survive page reloads
|
||||||
|
- Use `slices-pattern.ts` for large, complex stores (100+ lines)
|
||||||
|
- Use `nextjs-store.ts` for Next.js projects with SSR
|
||||||
|
|
||||||
|
### References (references/)
|
||||||
|
|
||||||
|
Deep-dive documentation for complex scenarios:
|
||||||
|
|
||||||
|
- `middleware-guide.md` - Complete middleware documentation (persist, devtools, immer, custom)
|
||||||
|
- `typescript-patterns.md` - Advanced TypeScript patterns and troubleshooting
|
||||||
|
- `nextjs-hydration.md` - SSR, hydration, and Next.js best practices
|
||||||
|
- `migration-guide.md` - Migrating from Redux, Context API, or Zustand v4
|
||||||
|
|
||||||
|
**When Claude should load these:**
|
||||||
|
- Load `middleware-guide.md` when user asks about persistence, devtools, or custom middleware
|
||||||
|
- Load `typescript-patterns.md` when encountering complex type inference issues
|
||||||
|
- Load `nextjs-hydration.md` for Next.js-specific problems
|
||||||
|
- Load `migration-guide.md` when migrating from other state management solutions
|
||||||
|
|
||||||
|
### Scripts (scripts/)
|
||||||
|
|
||||||
|
- `check-versions.sh` - Verify Zustand version and compatibility
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
cd your-project/
|
||||||
|
~/.claude/skills/zustand-state-management/scripts/check-versions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Vanilla Store (Without React)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from 'zustand/vanilla'
|
||||||
|
|
||||||
|
const store = createStore<CounterStore>()((set) => ({
|
||||||
|
count: 0,
|
||||||
|
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Subscribe to changes
|
||||||
|
const unsubscribe = store.subscribe((state) => {
|
||||||
|
console.log('Count changed:', state.count)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
console.log(store.getState().count)
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
store.getState().increment()
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
unsubscribe()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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) => {
|
||||||
|
set(...(a as Parameters<typeof set>))
|
||||||
|
console.log(`[${name}]:`, get())
|
||||||
|
}
|
||||||
|
return f(loggedSet, get, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use custom middleware
|
||||||
|
const useStore = create<MyStore>()(
|
||||||
|
logger((set) => ({
|
||||||
|
// store definition
|
||||||
|
}), 'MyStore'),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Immer Middleware (Mutable Updates)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { immer } from 'zustand/middleware/immer'
|
||||||
|
|
||||||
|
interface TodoStore {
|
||||||
|
todos: Array<{ id: string; text: string }>
|
||||||
|
addTodo: (text: 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 })
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**Required**:
|
||||||
|
- `zustand@5.0.8` - State management library
|
||||||
|
- `react@18.0.0+` - React framework
|
||||||
|
|
||||||
|
**Optional**:
|
||||||
|
- `@types/node` - For TypeScript path resolution
|
||||||
|
- `immer` - For mutable update syntax
|
||||||
|
- Redux DevTools Extension - For devtools middleware
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
|
||||||
|
- **Zustand**: https://zustand.docs.pmnd.rs/
|
||||||
|
- **GitHub**: https://github.com/pmndrs/zustand
|
||||||
|
- **TypeScript Guide**: https://zustand.docs.pmnd.rs/guides/typescript
|
||||||
|
- **Slices Pattern**: https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md
|
||||||
|
- **Context7 Library ID**: `/pmndrs/zustand`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package Versions (Verified 2025-10-24)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"zustand": "^5.0.8",
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Compatibility**:
|
||||||
|
- React 18+, React 19 ✅
|
||||||
|
- TypeScript 5+ ✅
|
||||||
|
- Next.js 14+, Next.js 15+ ✅
|
||||||
|
- Vite 5+ ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: Store updates don't trigger re-renders
|
||||||
|
**Solution**: Ensure you're using selector functions, not destructuring: `const bears = useStore(state => state.bears)` not `const { bears } = useStore()`
|
||||||
|
|
||||||
|
### Problem: TypeScript errors with middleware
|
||||||
|
**Solution**: Use double parentheses: `create<T>()()` not `create<T>()`
|
||||||
|
|
||||||
|
### Problem: Persist middleware causes hydration error
|
||||||
|
**Solution**: Implement `_hasHydrated` flag pattern (see Issue #1)
|
||||||
|
|
||||||
|
### Problem: Actions not showing in Redux DevTools
|
||||||
|
**Solution**: Pass action name as third parameter to `set`: `set(newState, undefined, 'actionName')`
|
||||||
|
|
||||||
|
### Problem: Store state resets unexpectedly
|
||||||
|
**Solution**: Check if using HMR (hot module replacement) - Zustand resets on module reload in development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Setup Checklist
|
||||||
|
|
||||||
|
Use this checklist to verify your Zustand setup:
|
||||||
|
|
||||||
|
- [ ] Installed `zustand@5.0.8` or later
|
||||||
|
- [ ] Created store with proper TypeScript types
|
||||||
|
- [ ] Used `create<T>()()` double parentheses syntax
|
||||||
|
- [ ] Tested selector functions in components
|
||||||
|
- [ ] Verified components only re-render when selected state changes
|
||||||
|
- [ ] If using persist: Configured unique storage name
|
||||||
|
- [ ] If using persist: Implemented hydration check for Next.js
|
||||||
|
- [ ] If using devtools: Named actions for debugging
|
||||||
|
- [ ] If using slices: Properly typed `StateCreator` for each slice
|
||||||
|
- [ ] All actions are pure functions
|
||||||
|
- [ ] No direct state mutations
|
||||||
|
- [ ] Store works in production build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions? Issues?**
|
||||||
|
|
||||||
|
1. Check [references/typescript-patterns.md](references/typescript-patterns.md) for TypeScript help
|
||||||
|
2. Check [references/nextjs-hydration.md](references/nextjs-hydration.md) for Next.js issues
|
||||||
|
3. Check [references/middleware-guide.md](references/middleware-guide.md) for persist/devtools help
|
||||||
|
4. Official docs: https://zustand.docs.pmnd.rs/
|
||||||
|
5. GitHub issues: https://github.com/pmndrs/zustand/issues
|
||||||
14
assets/example-template.txt
Normal file
14
assets/example-template.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[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.
|
||||||
109
plugin.lock.json
Normal file
109
plugin.lock.json
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:jezweb/claude-skills:skills/zustand-state-management",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "66e9a5da10c3fc3bbbc79fa955a12af56dc0d1f8",
|
||||||
|
"treeHash": "a4aa587f60dc89e7fda9871c379f9f86cf4a57fe716d14f9de73f6a1e2b63947",
|
||||||
|
"generatedAt": "2025-11-28T10:18:59.659661Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "zustand-state-management",
|
||||||
|
"description": "Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "45143f23a412d833a692ea801ba8dd376ca4baaea62074c4818884e3790442b6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "bf7cae6a656ce5a6969503f8cada38364ea3aeeb9f9d166d91b4288efcfc381d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/example-reference.md",
|
||||||
|
"sha256": "77c788d727d05d6479a61d6652b132e43882ffc67c145bb46ba880567d83f7f8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/nextjs-hydration.md",
|
||||||
|
"sha256": "ff2def96df47ace91d0902ae01ee34cfb3c68e5fb299db7fdbe48b0d74f34162"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/migration-guide.md",
|
||||||
|
"sha256": "a59c0d560924d2d0c22b0f0cdb3d40eaef035d8f6cdbd408cd4fddb53e6f64cf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/middleware-guide.md",
|
||||||
|
"sha256": "421fc14143c5bc2651fcbb10083e6d531d0ae49d8de4161217e1360db55d31b5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/typescript-patterns.md",
|
||||||
|
"sha256": "c671dbe8c06c22c6a3aabcbdb2f98013bf0321fb53cc36160cfc87771bb34390"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/check-versions.sh",
|
||||||
|
"sha256": "aec3932e266b7affd59830a5ead12a9d10d21d27cfa92a589a6d450113e564fa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/example-script.sh",
|
||||||
|
"sha256": "83d2b09d044811608e17cbd8e66d993b1e9998c7bd3379a42ab81fbdba973e0e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "3e03b08c2dc1530bad96d64a1cf703a288fb1f3adbfca8dec00f3388d616afce"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/async-actions-store.ts",
|
||||||
|
"sha256": "a632d997c7f89ee56eae0cbd2684c88a4cba10bc876cf0f9a55332498af42788"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/computed-store.ts",
|
||||||
|
"sha256": "9f192a17951fab6fd260acdcae2abd9449f69e2af3e5bb1d479f0d4f4b8cb60b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/typescript-store.ts",
|
||||||
|
"sha256": "33f5ad98c9bcfe27ab9442b5ecc69688f37f81055a1df07ba7ccb17ddc6188fc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/nextjs-store.ts",
|
||||||
|
"sha256": "8e4d2ac1f0b08a080510b8f11917ecc78813fa0391f8203a91453df65a9926e5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/slices-pattern.ts",
|
||||||
|
"sha256": "05c3548a5901bcc34d1ae6f12146aa0638c243c769310b5cf8c7a0d41ed2643f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/persist-store.ts",
|
||||||
|
"sha256": "9413e38459af97597d2c0fd558267b8a78f99bc35125964e8bfd100f08b9ff90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/basic-store.ts",
|
||||||
|
"sha256": "55b975521c02fa360bbea92a676c5b6d7acbc1eba0d6f7b1a7a05745772a496f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/devtools-store.ts",
|
||||||
|
"sha256": "b0cbea6c84bdadac58cefd6fb186cc8531d145ea41fe9f2bd7c6c27679acca98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "assets/example-template.txt",
|
||||||
|
"sha256": "3f725c80d70847fd8272bf1400515ba753f12f98f3b294d09e50b54b4c1b024a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "a4aa587f60dc89e7fda9871c379f9f86cf4a57fe716d14f9de73f6a1e2b63947"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
26
references/example-reference.md
Normal file
26
references/example-reference.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# [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.
|
||||||
303
references/middleware-guide.md
Normal file
303
references/middleware-guide.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
|
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({ /* store */ }),
|
||||||
|
{ name: 'storage-name' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// localStorage (default)
|
||||||
|
storage: createJSONStorage(() => localStorage)
|
||||||
|
|
||||||
|
// sessionStorage
|
||||||
|
storage: createJSONStorage(() => sessionStorage)
|
||||||
|
|
||||||
|
// Custom storage
|
||||||
|
storage: createJSONStorage(() => customStorage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Partial Persistence
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
persist(
|
||||||
|
(set) => ({ /* store */ }),
|
||||||
|
{
|
||||||
|
name: 'storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
theme: state.theme,
|
||||||
|
// Don't persist sensitive data
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Migration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { devtools } from 'zustand/middleware'
|
||||||
|
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
devtools(
|
||||||
|
(set) => ({ /* store */ }),
|
||||||
|
{ name: 'StoreName' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Named Actions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
increment: () => set(
|
||||||
|
(state) => ({ count: state.count + 1 }),
|
||||||
|
undefined,
|
||||||
|
'counter/increment' // Shows in DevTools
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Toggle
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
devtools(
|
||||||
|
(set) => ({ /* store */ }),
|
||||||
|
{
|
||||||
|
name: 'Store',
|
||||||
|
enabled: process.env.NODE_ENV === 'development'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Immer Middleware
|
||||||
|
|
||||||
|
Allows mutable state updates (Immer handles immutability):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT: devtools wraps persist
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({ /* store */ }),
|
||||||
|
{ name: 'storage' }
|
||||||
|
),
|
||||||
|
{ name: 'Store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shows persist actions in DevTools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Combinations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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()
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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()
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
devtools<T>(
|
||||||
|
stateCreator: StateCreator<T>,
|
||||||
|
options?: {
|
||||||
|
name?: string // Store name in DevTools
|
||||||
|
enabled?: boolean // Enable/disable
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### immer()
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
immer<T>(
|
||||||
|
stateCreator: StateCreator<T>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Reset Store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const initialState = { count: 0 }
|
||||||
|
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...initialState,
|
||||||
|
reset: () => set(initialState),
|
||||||
|
}),
|
||||||
|
{ name: 'storage' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear Persisted Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
302
references/migration-guide.md
Normal file
302
references/migration-guide.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ v4
|
||||||
|
const useStore = create<Store>((set) => ({ /* ... */ }))
|
||||||
|
|
||||||
|
// ✅ v5
|
||||||
|
const useStore = create<Store>()((set) => ({ /* ... */ }))
|
||||||
|
// ^^ Double parentheses required
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Persist Middleware Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG
|
||||||
|
create<Store>((set) => ({ /* ... */ }))
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
create<Store>()((set) => ({ /* ... */ }))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Creating Objects in Selectors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 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 `useSelector` to Zustand selectors
|
||||||
|
- [ ] Updated all `useDispatch` to direct function calls
|
||||||
|
- [ ] Added `persist` if state needs persistence
|
||||||
|
- [ ] Added `devtools` if 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
|
||||||
274
references/nextjs-hydration.md
Normal file
274
references/nextjs-hydration.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'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)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
function Page({ serverData }) {
|
||||||
|
const setData = useStore((state) => state.setData)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData(serverData)
|
||||||
|
}, [serverData, setData])
|
||||||
|
|
||||||
|
return <Component />
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Rendering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// next.config.js
|
||||||
|
module.exports = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check for Differences
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
281
references/typescript-patterns.md
Normal file
281
references/typescript-patterns.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# Zustand TypeScript Advanced Patterns
|
||||||
|
|
||||||
|
Advanced TypeScript patterns and troubleshooting. Load when encountering complex type inference issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic TypeScript Setup
|
||||||
|
|
||||||
|
### The Golden Rule: Double Parentheses
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT
|
||||||
|
const useStore = create<Store>()((set) => ({ /* ... */ }))
|
||||||
|
|
||||||
|
// ❌ WRONG
|
||||||
|
const useStore = create<Store>((set) => ({ /* ... */ }))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Currying syntax enables middleware type inference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Store Interface Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({ /* ... */ }),
|
||||||
|
{ name: 'storage' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Multiple Middlewares
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const useStore = create<Store>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({ /* ... */ }),
|
||||||
|
{ name: 'storage' }
|
||||||
|
),
|
||||||
|
{ name: 'Store' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slices with Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bears = useStore((state) => state.bears)
|
||||||
|
// Type: number
|
||||||
|
```
|
||||||
|
|
||||||
|
### Computed Selector
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const selectTotal = (state: Store) => state.items.reduce((sum, item) => sum + item.price, 0)
|
||||||
|
|
||||||
|
const total = useStore(selectTotal)
|
||||||
|
// Type: number
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Selector
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const selectById = (id: string) => (state: Store) =>
|
||||||
|
state.items.find((item) => item.id === id)
|
||||||
|
|
||||||
|
const item = useStore(selectById('123'))
|
||||||
|
// Type: Item | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vanilla Store Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG
|
||||||
|
const useStore = create<Store>((set) => ({ /* ... */ }))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use double parentheses
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT
|
||||||
|
const useStore = create<Store>()((set) => ({ /* ... */ }))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: StateCreator types fail
|
||||||
|
|
||||||
|
**Problem**: Missing middleware mutators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG
|
||||||
|
const createSlice: StateCreator<CombinedStore, [], [], MySlice>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Add middleware mutators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AllSlices = BearSlice & FishSlice & SharedSlice
|
||||||
|
|
||||||
|
const createBearSlice: StateCreator<AllSlices, [], [], BearSlice> = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Official TypeScript Guide
|
||||||
|
|
||||||
|
https://zustand.docs.pmnd.rs/guides/typescript
|
||||||
141
scripts/check-versions.sh
Executable file
141
scripts/check-versions.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/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
|
||||||
15
scripts/example-script.sh
Executable file
15
scripts/example-script.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/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]
|
||||||
309
templates/async-actions-store.ts
Normal file
309
templates/async-actions-store.ts
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
41
templates/basic-store.ts
Normal file
41
templates/basic-store.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*/
|
||||||
214
templates/computed-store.ts
Normal file
214
templates/computed-store.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
141
templates/devtools-store.ts
Normal file
141
templates/devtools-store.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
109
templates/nextjs-store.ts
Normal file
109
templates/nextjs-store.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*/
|
||||||
119
templates/persist-store.ts
Normal file
119
templates/persist-store.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 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!
|
||||||
|
*/
|
||||||
217
templates/slices-pattern.ts
Normal file
217
templates/slices-pattern.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
84
templates/typescript-store.ts
Normal file
84
templates/typescript-store.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user