add-store
Add a new Zustand store to ProAnimate with Immer middleware and Zundo undo/redo support. Includes TypeScript types and barrel export.
SKILL.mdAutoAnimation/.claude/skills/add-store/SKILL.md
GitHubAdd Zustand Store to ProAnimate
Create a new Zustand store following the project's conventions: Immer middleware for immutable updates and optional Zundo temporal middleware for undo/redo.
Steps
Create the store file at
src/stores/use<Name>Store.tsBasic store with Immer:
import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface MyItem { id: string name: string // ... item properties position: { x: number; y: number } visible: boolean startFrame: number endFrame: number } interface MyFeatureState { items: MyItem[] selectedId: string | null // Actions addItem: (item: Omit<MyItem, 'id'>) => void removeItem: (id: string) => void updateItem: (id: string, updates: Partial<MyItem>) => void setSelectedId: (id: string | null) => void clearAll: () => void } export const useMyFeatureStore = create<MyFeatureState>()( immer((set) => ({ items: [], selectedId: null, addItem: (item) => set((state) => { const id = `my-feature-${Date.now()}` state.items.push({ ...item, id }) state.selectedId = id }), removeItem: (id) => set((state) => { state.items = state.items.filter((i) => i.id !== id) if (state.selectedId === id) { state.selectedId = null } }), updateItem: (id, updates) => set((state) => { const item = state.items.find((i) => i.id === id) if (item) Object.assign(item, updates) }), setSelectedId: (id) => set((state) => { state.selectedId = id }), clearAll: () => set((state) => { state.items = [] state.selectedId = null }), })) )Add Zundo undo/redo (for stores that track user-editable state):
import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' import { temporal } from 'zundo' export const useMyFeatureStore = create<MyFeatureState>()( temporal( immer((set) => ({ // ... same as above })), { limit: 50 } ) ) // Export undo hook export const useMyFeatureUndo = () => useMyFeatureStore.temporal.getState()Add barrel export in
src/stores/index.ts:export { useMyFeatureStore } from './useMyFeatureStore'Exception: If the store imports heavy services (like the orchestrator store), do NOT add it to the barrel export. Instead, import it directly where needed to avoid eager loading. Add a comment explaining why:
// NOTE: useMyFeatureStore is intentionally NOT re-exported from the barrel. // It imports heavyService.ts which pulls in many dependencies. // Components import it directly: import { useMyFeatureStore } from '@/stores/useMyFeatureStore'Access other stores from within actions:
import { useTimelineStore } from './useTimelineStore' import { usePlaybackStore } from './usePlaybackStore' // Inside an action: addItem: () => set((state) => { const { totalFrames } = useTimelineStore.getState() const { fps } = usePlaybackStore.getState() // Use these values... }),
Key Files
| Purpose | Path |
|---|---|
| Store directory | src/stores/ |
| Barrel export | src/stores/index.ts |
| Timeline store (for frame data) | src/stores/useTimelineStore.ts |
| Playback store (for fps/frame) | src/stores/usePlaybackStore.ts |
| Canvas store (for dimensions) | src/stores/useCanvasStore.ts |
Store Conventions
- ID generation: Use
feature-${Date.now()}for unique IDs - Immer mutations: Always mutate
statedirectly insideset()-- Immer handles immutability - Cross-store reads: Use
otherStore.getState()to read from other stores (never subscribe inside actions) - No async in set(): For async operations, do the async work first, then call
set()with the results - Frame ranges: Items displayed on canvas typically have
startFrameandendFrameproperties. InitializeendFramefromuseTimelineStore.getState().totalFrames
Common Issues
- Stale state in callbacks: Use
useStore.getState()inside event handlers or async callbacks, not the hook return value. - Immer not working: Ensure you wrap with
immer()middleware. Without it, you must return new objects fromset(). - Circular imports: If store A imports store B and vice versa, use
getState()inside actions (lazy access) instead of top-level imports of the store instance. - Barrel export causes slowdown: If the store imports heavy modules, skip the barrel export and import directly to avoid eager loading at app startup.
Example
Reference stores:
- Simple:
src/stores/useArtCurveStore.ts-- Immer, CRUD for compositions, referencesuseTimelineStore - With timeline:
src/stores/useShapeStore.ts-- Shapes with position, frame ranges - Complex:
src/stores/useMultiCharacterStore.ts-- Multi-character dialogue with cross-store dependencies