zustand-game-patterns

Zustand Game Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zustand-game-patterns" with this command: npx skills add ccalebcarter/purria-skills/ccalebcarter-purria-skills-zustand-game-patterns

Zustand Game Patterns

Production-ready patterns for managing complex game state with Zustand.

Store Architecture

Modular Store Pattern

Split large game state into focused slices:

// stores/slices/timeSlice.ts import { StateCreator } from 'zustand'; import { GameState } from '../types';

export interface TimeSlice { time: GameTime; advancePhase: () => void; setDay: (day: number) => void; }

export const createTimeSlice: StateCreator< GameState, [['zustand/immer', never]], [], TimeSlice

= (set) => ({ time: { season: 1, day: 1, phase: 'morning' },

advancePhase: () => set((state) => { const phases = ['morning', 'action', 'resolution', 'night']; const idx = phases.indexOf(state.time.phase); state.time.phase = phases[(idx + 1) % 4]; if (idx === 3) state.time.day = Math.min(state.time.day + 1, 42); }),

setDay: (day) => set((state) => { state.time.day = Math.max(1, Math.min(day, 42)); }), });

Combining Slices

// stores/gameStore.ts import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; import { subscribeWithSelector } from 'zustand/middleware'; import { createTimeSlice, TimeSlice } from './slices/timeSlice'; import { createResourceSlice, ResourceSlice } from './slices/resourceSlice'; import { createHexSlice, HexSlice } from './slices/hexSlice';

type GameState = TimeSlice & ResourceSlice & HexSlice;

export const useGameStore = create<GameState>()( subscribeWithSelector( immer((...args) => ({ ...createTimeSlice(...args), ...createResourceSlice(...args), ...createHexSlice(...args), })) ) );

Persistence

Local Storage Save/Load

import { persist, createJSONStorage } from 'zustand/middleware';

export const useGameStore = create<GameState>()( persist( subscribeWithSelector( immer((set, get) => ({ // ... state and actions })) ), { name: 'game-save', storage: createJSONStorage(() => localStorage),

  // Only persist specific fields
  partialize: (state) => ({
    time: state.time,
    resources: state.resources,
    score: state.score,
    // Exclude transient state like selectedHex
  }),
  
  // Handle version migrations
  version: 1,
  migrate: (persisted, version) => {
    if (version === 0) {
      // Migration from v0 to v1
      return { ...persisted, newField: 'default' };
    }
    return persisted;
  },
}

) );

Multiple Save Slots

interface SaveSlot { id: string; name: string; timestamp: number; data: Partial<GameState>; }

const SAVE_SLOTS_KEY = 'game-saves';

export const saveToSlot = (slotId: string, name: string) => { const state = useGameStore.getState(); const saves = JSON.parse(localStorage.getItem(SAVE_SLOTS_KEY) || '[]');

const saveData: SaveSlot = { id: slotId, name, timestamp: Date.now(), data: { time: state.time, resources: state.resources, score: state.score, hexes: Object.fromEntries(state.hexes), }, };

const idx = saves.findIndex((s: SaveSlot) => s.id === slotId); if (idx >= 0) saves[idx] = saveData; else saves.push(saveData);

localStorage.setItem(SAVE_SLOTS_KEY, JSON.stringify(saves)); };

export const loadFromSlot = (slotId: string) => { const saves = JSON.parse(localStorage.getItem(SAVE_SLOTS_KEY) || '[]'); const slot = saves.find((s: SaveSlot) => s.id === slotId);

if (slot) { useGameStore.setState({ ...slot.data, hexes: new Map(Object.entries(slot.data.hexes || {})), }); } };

Undo/Redo (Time Travel)

import { temporal } from 'zundo';

export const useGameStore = create<GameState>()( temporal( immer((set) => ({ // ... state and actions })), { // Limit history size limit: 50,

  // Only track specific changes
  partialize: (state) => ({
    hexes: state.hexes,
    resources: state.resources,
  }),
  
  // Equality check to prevent duplicate history
  equality: (a, b) => JSON.stringify(a) === JSON.stringify(b),
}

) );

// Usage const { undo, redo, pastStates, futureStates } = useGameStore.temporal.getState();

Subscriptions & Side Effects

Subscribe to State Changes

// Subscribe outside React const unsubscribe = useGameStore.subscribe( (state) => state.time.phase, (phase, prevPhase) => { console.log(Phase changed: ${prevPhase} → ${phase});

// Trigger side effects
if (phase === 'morning') {
  useGameStore.getState().spawnTrouble();
}

} );

// Subscribe to multiple selectors useGameStore.subscribe( (state) => ({ day: state.time.day, phase: state.time.phase }), ({ day, phase }) => { // Analytics tracking analytics.track('game_progress', { day, phase }); }, { equalityFn: shallow } );

React Subscription Hook

import { useEffect } from 'react'; import { useShallow } from 'zustand/react/shallow';

// Optimized selector with shallow comparison export const useGameTime = () => useGameStore( useShallow((state) => ({ day: state.time.day, phase: state.time.phase, season: state.time.season, })) );

// Effect on state change export const usePhaseEffects = () => { const phase = useGameStore((s) => s.time.phase);

useEffect(() => { if (phase === 'resolution') { // Play resolution animation playSound('phase_change'); } }, [phase]); };

Performance Optimization

Selector Memoization

// ❌ Bad: Creates new object every render const { time, resources } = useGameStore((state) => ({ time: state.time, resources: state.resources, }));

// ✅ Good: Use shallow comparison import { useShallow } from 'zustand/react/shallow';

const { time, resources } = useGameStore( useShallow((state) => ({ time: state.time, resources: state.resources, })) );

// ✅ Best: Separate selectors for independent updates const time = useGameStore((s) => s.time); const resources = useGameStore((s) => s.resources);

Computed Selectors

// Create memoized selectors for derived state import { createSelector } from 'reselect';

const selectTroubles = (state: GameState) => state.troubles; const selectGridSize = (state: GameState) => state.gridSize;

export const selectTroubleCount = createSelector( [selectTroubles], (troubles) => Object.keys(troubles).length );

export const selectTotalTroubleHexes = createSelector( [selectTroubles], (troubles) => Object.values(troubles) .reduce((sum, t) => sum + t.hexCoords.length, 0) );

// Usage const troubleCount = useGameStore(selectTroubleCount);

Batched Updates

// Batch multiple state changes const endDay = () => { useGameStore.setState((state) => { // All updates in single render state.metaPots.activeBets = []; state.time.phase = 'morning'; state.time.day += 1; state.resources.stamina = 100; }); };

Devtools Integration

import { devtools } from 'zustand/middleware';

export const useGameStore = create<GameState>()( devtools( subscribeWithSelector( immer((set) => ({ // ... state and actions

    // Named actions for devtools
    advancePhase: () => set(
      (state) => { /* ... */ },
      false,
      'time/advancePhase' // Action name in devtools
    ),
  }))
),
{
  name: 'GameStore',
  enabled: process.env.NODE_ENV === 'development',
}

) );

Testing Patterns

// Reset store between tests beforeEach(() => { useGameStore.setState({ time: { season: 1, day: 1, phase: 'morning' }, resources: { tulipBulbs: 10, coins: 3000, stamina: 100 }, // ... initial state }); });

// Test actions test('advancePhase cycles through phases', () => { const { advancePhase } = useGameStore.getState();

expect(useGameStore.getState().time.phase).toBe('morning'); advancePhase(); expect(useGameStore.getState().time.phase).toBe('action'); advancePhase(); expect(useGameStore.getState().time.phase).toBe('resolution'); });

// Test subscriptions test('spawns trouble on morning phase', () => { const spawnTrouble = vi.spyOn(useGameStore.getState(), 'spawnTrouble');

// Advance to morning useGameStore.setState({ time: { ...initialTime, phase: 'night' } }); useGameStore.getState().advancePhase();

expect(spawnTrouble).toHaveBeenCalled(); });

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

game-engineering-team

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-game-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

game-assets-team

No summary provided by upstream source.

Repository SourceNeeds Review
General

game-concept-advisor

No summary provided by upstream source.

Repository SourceNeeds Review