store-slices
Create Zustand store slices for UI-only state management
When & Why to Use This Skill
This Claude skill automates the creation of Zustand store slices specifically designed for UI-only state management. It enforces a clean architectural separation between volatile UI states and core game logic (ECS), providing standardized TypeScript templates that ensure type safety, maintainability, and seamless synchronization with external systems.
Use Cases
- Decoupling UI and Game Logic: Creating dedicated slices for interface elements like menu visibility, UI scaling, and selection states while keeping the primary game data within an Entity Component System (ECS).
- Synchronized State Mutations: Implementing action slices that trigger complex system mutations and automatically update the UI state only upon successful execution, ensuring the UI remains a faithful reflection of the source of truth.
- Standardizing Frontend Architecture: Enforcing consistent boilerplate and naming conventions (e.g., *Slice.ts) across a development team to reduce technical debt and improve code readability in large-scale React applications.
| name | store-slices |
|---|---|
| description | Create Zustand store slices for UI-only state management |
Store Slices
Write Zustand slices in src/stores/slices/ for UI-only state.
Requirements
- File naming:
*Slice.ts(e.g.,uiSlice.ts) - UI state only - game data belongs in ECS
- Call ECS systems for mutations, then sync UI state
Pattern
import type { StateCreator } from 'zustand';
import { someSystem } from '../../ecs/systems/someSystem';
import { getQuery } from '../../ecs/queries/someQueries';
// State interface
interface SliceNameState {
selectedId: string | null;
isOpen: boolean;
}
// Actions interface
interface SliceNameActions {
select: (id: string) => void;
toggle: () => void;
performAction: (id: string) => boolean;
}
// Combined slice type
export interface SliceNameSlice extends SliceNameState, SliceNameActions {}
// Initial state
const initialState: SliceNameState = {
selectedId: null,
isOpen: false,
};
// Slice creator
export const createSliceNameSlice: StateCreator<SliceNameSlice, [], [], SliceNameSlice> = (
set,
get
) => ({
...initialState,
select: (id): void => {
set({ selectedId: id });
},
toggle: (): void => {
set((state) => ({ isOpen: !state.isOpen }));
},
performAction: (id): boolean => {
// Call ECS system
const result = someSystem({ entityId: id });
if (!result.success) return false;
// Sync UI state from ECS
const data = getQuery();
set({ selectedId: null, ...data });
return true;
},
});
Rules
- UI state only - Never store game data (items, grids, conditions)
- ECS is source of truth - Call ECS systems for mutations
- Sync after ECS calls - Update UI state after successful ECS operations
- Return success/failure - Actions that call ECS should return boolean
- Separate state and actions - Define interfaces for both
Slice Categories
Pure UI State
State with no ECS interaction:
uiSlice- selection, scale, menu visibilityinputModeSlice- keyboard vs pointer mode
ECS-Synced State
State that mirrors or derives from ECS:
equipmentSlice- mirrors equipment from ECSnavigationSlice- focus paths derived from ECS grids
Action Slices
Actions that call ECS systems:
equipmentActionsSlice- equip, unequip, destroy items
Combining Slices
In main store file:
import { create } from 'zustand';
import { createSliceASlice } from './slices/sliceASlice';
import { createSliceBSlice } from './slices/sliceBSlice';
type StoreState = SliceASlice & SliceBSlice;
export const useStore = create<StoreState>()((...args) => ({
...createSliceASlice(...args),
...createSliceBSlice(...args),
}));
What Belongs in Zustand vs ECS
| Zustand (UI State) | ECS (Game State) |
|---|---|
| Selected item ID | Item entities and components |
| Menu open/closed | Grid contents |
| Focus path | Equipment slots |
| Input mode | Character conditions |
| UI scale | Item positions |