Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:53 +08:00
commit 36a2b87167
20 changed files with 3513 additions and 0 deletions

View 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
View 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
View 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

View 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
View 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": []
}
}

View 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.

View 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

View 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

View 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

View 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
View 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
View 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]

View 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
View 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
View 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
View 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
View 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
View 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
View 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
*/

View 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>
* }
*/