<< All versions

Skill v1.0.0

currentAutomated scan100/100
komluk/scaffolding/state-management
──Details
PublishedOctober 3, 2026 at 02:50 PM
Content Hashsha256:90756f57c767a4eb...
Git SHA
──Files
Files (1 file, 7.7 KB)
SKILL.md7.7 KBactive
SKILL.md · 243 lines · 7.7 KB

version: "1.0.0" name: state-management description: "Client state management standards: local-vs-global decisions, store structure, selectors, and re-render performance. TRIGGER when: creating a store, managing global state, or deciding local vs global state. SKIP: component/hook structure (use react-patterns); visual styling (use mui-styling). (Examples use Zustand.)"


State Management Skill

Client state management standards and best practices. The universal guidance (local-vs-global decisions, store structure, selectors, performance) applies to any store library; the concrete code examples use Zustand.

When to Apply

  • Creating new state stores
  • Sharing state between components
  • State architecture decisions
  • Performance optimization for state

State Category Guidelines

CategorySolutionUse When
Local UIuseStateSingle component, simple state
Shared UIZustand storeMultiple components need same data
Server dataReact Query / SWRAPI data with caching needs
Form stateuseState / form libraryForm inputs, validation
URL stateuseSearchParamsFilter/sort params, shareable URLs

Zustand Store Standards

Store Structure

ElementRequirement
InterfaceDefine typed state + actions interface
StateGroup related state together
ActionsDefine all mutations as functions
Naminguse[Domain]Store convention

Store Design Principles

PrincipleDescription
Single responsibilityOne store per domain (projects, UI, settings)
Flat structureAvoid deeply nested state
Immutable updatesAlways return new objects
Colocated actionsKeep actions inside store definition

Selector Best Practices

Selection Rules

RuleReason
Select minimal dataReduces re-renders
Use shallow for objectsPrevents unnecessary updates
Memoize derived dataUse useMemo for computed values
Avoid selecting entire storeCauses re-render on any change

Selector Patterns

PatternUse Case
Single value(state) => state.count
Multiple values(state) => ({ a: state.a, b: state.b }), shallow
Derived valueuseMemo outside store

Middleware Usage

MiddlewarePurposeWhen to Use
persistLocalStorage persistenceUser preferences, settings
devtoolsRedux DevTools integrationDevelopment debugging
immerImmutable updatesComplex nested state
subscribeWithSelectorGranular subscriptionsPerformance optimization

Store Organization

File Structure

src/stores/
├── index.ts # Re-exports all stores
├── projectStore.ts # Domain-specific store
├── uiStore.ts # UI state (modals, panels)
└── settingsStore.ts # User preferences

Store Separation Guidelines

Store TypeContains
Domain storeBusiness entities, selections
UI storeModal states, panel visibility, loading
Settings storeUser preferences, persisted config

Performance Guidelines

DO

  • Select only needed state slices
  • Use shallow equality for object selections
  • Define actions inside store (stable references)
  • Split large stores by domain
  • Use subscribeWithSelector for side effects

DON'T

  • Select entire store object
  • Create new objects in selectors without shallow
  • Put derived/computed state in store
  • Call actions during render phase
  • Create deeply nested store structure

Anti-Patterns

Anti-PatternProblemSolution
Giant storeHard to maintain, performance issuesSplit by domain
Computed in storeStale data, extra complexityUse useMemo
Prop drillingBypasses store benefitsUse store directly
Store in useStateLoses reactivityUse store hook
No TypeScriptRuntime errorsDefine interfaces

Decision Matrix

ScenarioRecommended Solution
Form input stateuseState
Modal open/closeuseState or UI store
Selected item (shared)Domain store
User preferencesPersisted store
API response dataStore + service function
Computed/derived datauseMemo from store values
Global loading stateUI store

Example: Zustand Store Patterns (illustrative)

Illustrative — these are one team's concrete Zustand conventions shown as an
example. Substitute your store library's equivalents (Redux Toolkit, Jotai,
Pinia, signals, etc.). The decision matrix and selector/performance guidance
above are the reusable, library-agnostic part.

Store Files (<your-stores-module>/)

StorePersistPurpose
workspaceStore.tsYes (scaffolding-workspaces)Projects/workspaces, active selection
taskStore.tsNoTask CRUD, SSE updates, agent statuses
authStore.tsYes (auth-storage)GitHub OAuth state, user profile

Type Pattern: Separate State & Actions

Every store defines type XxxState and type XxxActions, then exports the combined type:

typescript
type WorkspaceState = {
workspaces: Workspace[];
activeWorkspace: Workspace | null;
};
type WorkspaceActions = {
addWorkspace: (pathOrWorkspace: string | Workspace) => void;
removeWorkspace: (path: string) => Promise<void>;
setActiveWorkspace: (path: string) => void;
loadFromApi: () => Promise<void>;
};
export type WorkspaceStore = WorkspaceState & WorkspaceActions;

Persist Middleware with partialize

Persisted stores use partialize to exclude actions and transient state from storage:

typescript
export const useWorkspaceStore = create<WorkspaceStore>()(
persist(
(set, get) => ({ /* state + actions */ }),
{
name: 'scaffolding-workspaces',
partialize: (state) => ({
workspaces: state.workspaces,
activeWorkspace: state.activeWorkspace,
}),
}
)
);

Hook Wrappers with useShallow (<your-hooks-module>/)

Stores are consumed through hook wrappers that use useShallow to prevent re-renders:

typescript
import { useWorkspaceStore } from '../stores/workspaceStore';
import { useShallow } from 'zustand/shallow';
export function useWorkspace() {
return useWorkspaceStore(
useShallow((state) => ({
workspaces: state.workspaces,
activeWorkspace: state.activeWorkspace,
addWorkspace: state.addWorkspace,
// ... selected fields only
}))
);
}

API Sync Pattern (loadFromApi)

Stores hydrate from localStorage first, then enrich from API:

  1. persist middleware loads cached state from localStorage on mount
  2. loadFromApi() fetches from backend, merges fields (id, github_url, role) into existing entries
  3. Failures are silently caught - localStorage data still works offline

SSE Update Pattern (taskStore)

Non-persisted stores receive real-time updates from Server-Sent Events:

  • addTask(task) - deduplicates before prepending to list
  • updateTask(task) - replaces full task object in list and currentTask
  • updateTaskPartial(id, updates) - merges partial fields from SSE completion events
  • updateAgentStatus(taskId, status) - tracks per-agent progress within a task

Error Handling in Stores

Stores use extractErrorMessage(err, fallbackMsg) utility and store errors in state:

typescript
catch (err) {
const message = extractErrorMessage(err, 'Failed to fetch tasks');
set({ error: message, isLoading: false });
}

For expected errors (e.g., 409 conflict on cancel), check AxiosError status before setting error state.

All versions