commit 36a2b871675d2c180859f17b1d459ca762e04ee1 Author: Zhongwei Li Date: Sun Nov 30 08:25:53 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a824cca --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f957c2a --- /dev/null +++ b/README.md @@ -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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..37f5634 --- /dev/null +++ b/SKILL.md @@ -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()((set) => ({ + bears: 0, + increase: (by) => set((state) => ({ bears: state.bears + by })), + reset: () => set({ bears: 0 }), +})) +``` + +**CRITICAL**: Notice the **double parentheses** `create()()` - 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

{bears} around here...

+} + +function Controls() { + const increase = useBearStore((state) => state.increase) + return +} +``` + +**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()((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()()` 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()( + 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()()` (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(...)` (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()( + 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
Loading...
+ } + + // Now safe to render with persisted state + return +} +``` + +### 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()()` is required for middleware to work with TypeScript inference. + +**Prevention**: +```typescript +// ❌ WRONG - Single parentheses +const useStore = create((set) => ({ + // ... +})) + +// ✅ CORRECT - Double parentheses +const useStore = create()((set) => ({ + // ... +})) +``` + +**Rule**: Always use `create()()` 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()((...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()( + 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()( + 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()( + 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()((set) => ({ + items: [], + addItem: (item) => set((state) => ({ items: [...state.items, item] })), +})) + +// Use in component +function ItemCount() { + const count = useStore((state) => state.items.length) + return
{count} items
+} +``` + +### Pattern: Async Actions + +```typescript +interface AsyncStore { + data: string | null + isLoading: boolean + error: string | null + fetchData: () => Promise +} + +const useAsyncStore = create()((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()((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()((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 ( +
+ toggleTodo(id)} + /> + {todo.text} +
+ ) +} +``` + +--- + +## 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()((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 = ( + f: StateCreator, + name?: string, +) => StateCreator + +const logger: Logger = (f, name) => (set, get, store) => { + const loggedSet: typeof set = (...a) => { + set(...(a as Parameters)) + console.log(`[${name}]:`, get()) + } + return f(loggedSet, get, store) +} + +// Use custom middleware +const useStore = create()( + 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()( + 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()()` not `create()` + +### 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()()` 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 diff --git a/assets/example-template.txt b/assets/example-template.txt new file mode 100644 index 0000000..349fec2 --- /dev/null +++ b/assets/example-template.txt @@ -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. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..0d83373 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/references/example-reference.md b/references/example-reference.md new file mode 100644 index 0000000..1be1b40 --- /dev/null +++ b/references/example-reference.md @@ -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. diff --git a/references/middleware-guide.md b/references/middleware-guide.md new file mode 100644 index 0000000..f969b6e --- /dev/null +++ b/references/middleware-guide.md @@ -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()( + 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()( + 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()( + 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()( + 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 = ( + f: StateCreator, + name?: string +) => StateCreator + +const logger: Logger = (f, name) => (set, get, store) => { + const loggedSet: typeof set = (...a) => { + set(...(a as Parameters)) + console.log(`[${name}]:`, get()) + } + return f(loggedSet, get, store) +} +``` + +--- + +## Middleware API Reference + +### persist() + +```typescript +persist( + stateCreator: StateCreator, + options: { + name: string // Storage key (required) + storage?: PersistStorage // Storage engine + partialize?: (state: T) => Partial // 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( + stateCreator: StateCreator, + options?: { + name?: string // Store name in DevTools + enabled?: boolean // Enable/disable + } +) +``` + +### immer() + +```typescript +immer( + stateCreator: StateCreator +) +``` + +--- + +## Common Patterns + +### Reset Store + +```typescript +const initialState = { count: 0 } + +const useStore = create()( + persist( + (set) => ({ + ...initialState, + reset: () => set(initialState), + }), + { name: 'storage' } + ) +) +``` + +### Clear Persisted Data + +```typescript +// Clear localStorage +localStorage.removeItem('storage-name') + +// Or programmatically +const useStore = create()( + 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 diff --git a/references/migration-guide.md b/references/migration-guide.md new file mode 100644 index 0000000..b52f9fc --- /dev/null +++ b/references/migration-guide.md @@ -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 + + + + +// Component +const count = useSelector((state) => state.count) +const dispatch = useDispatch() + +``` + +### After (Zustand) + +```typescript +// Store (all in one!) +import { create } from 'zustand' + +interface Store { + count: number + increment: () => void + decrement: () => void +} + +const useStore = create()((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) + +``` + +**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 ( + + {children} + + ) +} + +// Hook +function useCount() { + const context = useContext(CountContext) + if (!context) throw new Error('useCount must be within CountProvider') + return context +} + +// App + + + + +// Component +const { count, increment } = useCount() +``` + +### After (Zustand) + +```typescript +// Store +const useStore = create()((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((set) => ({ /* ... */ })) + +// ✅ v5 +const useStore = create()((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()( + 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((set) => ({ /* ... */ })) + +// ✅ CORRECT +create()((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()()` +- [ ] 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 diff --git a/references/nextjs-hydration.md b/references/nextjs-hydration.md new file mode 100644 index 0000000..7b848b7 --- /dev/null +++ b/references/nextjs-hydration.md @@ -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()( + 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
Loading...
+ } + + return
Count: {count}
+} +``` + +--- + +## 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
Count: {count}
+} +``` + +**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 ( + + {children} + + ) +} + +// app/page.tsx +'use client' + +import { useStore } from './store' + +export default function Page() { + return +} +``` + +**Note**: Add `'use client'` to components using Zustand. + +### Pages Router + +```typescript +// pages/_app.tsx +export default function App({ Component, pageProps }) { + return +} + +// pages/index.tsx +import { useStore } from '../store' + +export default function Page() { + return +} +``` + +**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
{count}
+} + +// ✅ CORRECT - Client Component +'use client' + +export default function ClientComponent() { + const count = useStore((state) => state.count) + return
{count}
+} +``` + +--- + +## 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 +} +``` + +--- + +## Common Patterns + +### Initialize Store from Props + +```typescript +'use client' + +function Page({ serverData }) { + const setData = useStore((state) => state.setData) + + useEffect(() => { + setData(serverData) + }, [serverData, setData]) + + return +} +``` + +### Conditional Rendering + +```typescript +'use client' + +function Component() { + const hasHydrated = useStore((state) => state._hasHydrated) + const theme = useStore((state) => state.theme) + + return ( +
+ {hasHydrated ? : } +
+ ) +} +``` + +### 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 ( +
+ + {hasHydrated && } +
+ ) +} +``` + +--- + +## 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 diff --git a/references/typescript-patterns.md b/references/typescript-patterns.md new file mode 100644 index 0000000..8fbb52d --- /dev/null +++ b/references/typescript-patterns.md @@ -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()((set) => ({ /* ... */ })) + +// ❌ WRONG +const useStore = create((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()((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()( + 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()( + persist( + (set) => ({ /* ... */ }), + { name: 'storage' } + ) +) +``` + +### With Multiple Middlewares + +```typescript +const useStore = create()( + 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()((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()((set) => ({ + bears: 0, + increase: () => set((state) => ({ bears: state.bears + 1 })), +})) + +// Create custom hook +function useBearStore(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((set) => ({ /* ... */ })) +``` + +**Solution**: Use double parentheses + +```typescript +// ✅ CORRECT +const useStore = create()((set) => ({ /* ... */ })) +``` + +### Error: StateCreator types fail + +**Problem**: Missing middleware mutators + +```typescript +// ❌ WRONG +const createSlice: StateCreator +``` + +**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 = ... +``` + +--- + +## Official TypeScript Guide + +https://zustand.docs.pmnd.rs/guides/typescript diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..617c793 --- /dev/null +++ b/scripts/check-versions.sh @@ -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 diff --git a/scripts/example-script.sh b/scripts/example-script.sh new file mode 100755 index 0000000..1c0c72e --- /dev/null +++ b/scripts/example-script.sh @@ -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] diff --git a/templates/async-actions-store.ts b/templates/async-actions-store.ts new file mode 100644 index 0000000..fdd8d4c --- /dev/null +++ b/templates/async-actions-store.ts @@ -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 + fetchPosts: (userId: string) => Promise + createPost: (post: Omit) => Promise + deletePost: (postId: string) => Promise + reset: () => void +} + +export const useAsyncStore = create()((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
Loading...
+ * if (error) return
Error: {error}
+ * if (!user) return
No user found
+ * + * return ( + *
+ *

{user.name}

+ *

{user.email}

+ *
+ * ) + * } + * + * 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
Loading posts...
+ * if (error) return
Error: {error}
+ * + * return ( + *
    + * {posts.map((post) => ( + *
  • + *

    {post.title}

    + *

    {post.body}

    + * + *
  • + * ))} + *
+ * ) + * } + * + * 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 ( + *
+ * setTitle(e.target.value)} + * placeholder="Title" + * /> + *