Frontend State Management Skill
Guides implementation of frontend state following the Backcast EVS architecture: TanStack Query for server state, Zustand + Immer for client state, with strict context isolation for versioned entities.
Quick Start
When implementing state for a new entity:
- •Determine Entity Type - Is it Versioned (Time Machine) or Simple?
- •Add Query Keys - Define in
frontend/src/api/queryKeys.ts - •Create CRUD Hooks - Use factory:
createVersionedResourceHooksorcreateResourceHooks - •Configure Dependent Invalidation - Add EVM/forecasts dependencies if needed
- •Write Component - Use hooks with proper context parameters
Entity Type Decision Guide
| Question | Versioned (Time Machine) | Simple |
|---|---|---|
| Need branch isolation? | Yes | No |
| Need history/audit trail? | Yes | No |
| Supports time-travel (asOf)? | Yes | No |
| User-editable data? | Usually Yes | Usually No |
| Examples | Projects, WBEs, Cost Elements, Change Orders, Forecasts | Users, Departments, Cost Element Types, Config |
Hook Factory Selection
Does entity support Time Machine (branch/asOf)?
├─ Yes → createVersionedResourceHooks() (useVersionedCrud.ts)
│ - Auto-injects { branch, asOf, mode } into query keys
│ - Context isolation prevents stale data
│ - Use for: Projects, WBEs, Cost Elements, Change Orders, Forecasts
└─ No → createResourceHooks() (useCrud.ts)
- No context injection
- Use for: Users, Departments, Cost Element Types, System Config
Implementation Checklist
1. Query Keys (frontend/src/api/queryKeys.ts)
- • Add entity to
queryKeysfactory with kebab-case naming - • Include
context?: anyparameter for versioned entities - • Define
all,lists(),list(),details(),detail(id, context?) - • Add domain-specific keys (e.g.,
breadcrumb,evmMetrics,history)
See example: frontend/src/api/queryKeys.ts
2. CRUD Hooks (frontend/src/features/[domain]/hooks/use[Entity].ts)
- • Use
createVersionedResourceHooksfor versioned entities - • Use
createResourceHooksfor simple entities - • Pass query key factory sub-key (e.g.,
queryKeys.costElements) - • Configure dependent invalidations
- • Export named hooks:
use[Entity]s,use[Entity],useCreate[Entity], etc.
See factory: frontend/src/hooks/useVersionedCrud.ts
3. Dependent Invalidation Configuration
Mutations must invalidate ALL dependent data to prevent stale EVM metrics.
| Primary Entity | Dependent Invalidations | Query Key Pattern |
|---|---|---|
| Cost Elements | forecasts.all | queryKeys.forecasts.all |
| Cost Registrations | forecasts.all, budgetStatus | Both keys |
| Schedule Baselines | forecasts.all | queryKeys.forecasts.all |
| Progress Entries | forecasts.all, evm.metrics, evm.timeSeries | All EVM-related keys |
| Change Orders | projects.branches(projectId), projects | Branch selectors + project lists |
See: State Management > Dependent Invalidation Strategy
4. Component Usage
- • Use
useTimeMachineParams()to get context for versioned entities - • Pass hooks to components (no manual fetching in useEffect)
- • Handle loading/error states properly
- • Use
enabledoption for conditional queries
Context Isolation Rules
Why Context Isolation Matters
Without context parameters, query keys are shared across branches/time periods:
- •Switching branches shows stale data from previous branch
- •Time-traveling shows incorrect historical state
- •Changes in one branch affect UI in other branches
Correct vs Incorrect Query Keys
❌ WRONG - Manual key construction WITHOUT context
queryKey: ["cost-elements", id] // Shared across ALL branches!
❌ WRONG - Factory WITHOUT context
queryKey: queryKeys.costElements.detail(id) // Missing context!
✅ CORRECT - Factory WITH context from TimeMachineContext
const { branch, asOf } = useTimeMachineParams();
queryKey: queryKeys.costElements.detail(id, { branch, asOf })
✅ CORRECT - Using createVersionedResourceHooks (auto-injects context)
const { data } = useCostElement(id); // Context auto-injected
Context Object Structure
The context object comes from useTimeMachineParams():
{
branch: string; // 'main', 'feature-branch', etc.
asOf?: string; // ISO timestamp or undefined for current
mode: 'merged' | 'isolated'; // Query mode
}
Optimistic Updates
Optimistic updates are required for high-frequency user actions (renaming, status changes).
See pattern: State Management > Optimistic Updates
Zustand Stores (Client State)
Use Zustand only for global UI state, not API data.
When to Use Zustand
| Use Case | Example State |
|---|---|
| Authentication | { token, user } |
| UI Preferences | { sidebarOpen, theme } |
| Modal/Drawer State | { isOpen, content } |
| Form Draft State | { drafts: {} } |
Zustand + Immer Pattern
immer(persist(...)) // Always wrap persist with immer
See examples in: State Management > Client State
Common Pitfalls
❌ Storing API Data in Zustand
API data should use TanStack Query hooks, not Zustand stores.
❌ Using useEffect for Data Fetching
❌ useEffect(() => { fetchCostElements().then(setData); }, []);
✅ const { data } = useCostElements();
❌ Missing Context in Query Keys
❌ detail: (id: string) => ["cost-elements", "detail", id] as const ✅ detail: (id: string, context?: any) => ["cost-elements", "detail", id, context] as const
❌ Incomplete Dependent Invalidation
❌ Only invalidates the entity itself (missing forecasts!) ✅ Invalidates entity + dependent EVM data
❌ Using createResourceHooks for Versioned Entities
❌ createResourceHooks("cost-elements", {...}) // No context injection
✅ createVersionedResourceHooks("cost-elements", queryKeys.costElements, {...})
Quality Gates
Before completion, ensure:
- • Query keys use centralized factory (
queryKeys.ts) - • Versioned entities include
context?: anyparameter - • Correct hook factory used (versioned vs simple)
- • Dependent invalidations configured for EVM-related entities
- • No API data stored in Zustand stores
- • No manual data fetching with useEffect
- • Optimistic updates for high-frequency actions
- • TypeScript strict mode passes (zero errors)
- • ESLint passes (zero errors)
Code References
| Concept | File |
|---|---|
| Query Key Factory | frontend/src/api/queryKeys.ts |
| Versioned Hooks | frontend/src/hooks/useVersionedCrud.ts |
| Simple Hooks | frontend/src/hooks/useCrud.ts |
| Time Machine Context | frontend/src/contexts/TimeMachineContext.tsx |
| API Client | frontend/src/api/client.ts |
Related Documentation
- •State Management Architecture - Complete state management guide
- •Frontend Coding Standards - Frontend standards reference
- •Time Machine Context - Time travel and branching
Out of Scope
This skill does NOT:
- •Create backend entities or API endpoints (use backend-entity-dev skill)
- •Handle React component structure and props (use frontend-developer agent)
- •Manage database migrations or backend schemas
- •Implement authentication/authorization logic