| name | client-state-management |
| description | Use when adding or changing client state, React Query data flow, immutable context values, or Zustand stores. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/client-state-management.md.
Referenced context:
../../../ai/rules/client-state-rules.md
../../../ai/rules/client-api-hook-rules.md
../../../ai/architecture/client-app.md
../../../ai/examples/good-client-api-operation.md
This file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
Compiled AI Skill: client-state-management
Canonical Skill: ai/skills/client-state-management.md
Client State Management
Use this skill when adding or changing client-side state, React Query data flow, immutable context values, or Zustand stores in apps/client.
Goal
Place each state concern at the narrowest correct owner without duplicating server data or creating mutable global context.
Read First
ai/rules/client-state-rules.md
ai/rules/client-api-hook-rules.md
ai/architecture/client-app.md
ai/examples/good-client-api-operation.md
Workflow
- Classify the state as server/cache, route/navigation, page workflow, component-only, cross-route client state, or stable service.
- Keep server state in React Query hooks.
- Keep mutation freshness in mutation hook invalidation.
- Use IDs and derived data instead of duplicated entity objects.
- Add Zustand only after React Query, router state, local state, and focused hooks are insufficient.
Expected Output
- Components do not copy query data into local state or Zustand just to pass it down.
- Components do not import query keys directly.
- State reset rules live next to the state owner.
- Context values are stable services/constants, not mutable UI state.
Verification
- Search changed components for copied query data in
useState or Zustand.
- Search changed components for direct query-key imports.
- Run
npm --workspace @capture-flag/client run build.
Referenced Context
Reference: ai/rules/client-state-rules.md
Client State Rules
Rules for state ownership in apps/client.
Always
- Use React Query as the source of truth for server state.
- Let mutation hooks own cache invalidation with
queryClient.invalidateQueries.
- Use local component state for short-lived UI state owned by one component.
- Lift state only to the nearest common owner that explicitly consumes it.
- Use React Router params or search params for linkable, reload-safe, navigation state.
- Keep selection state as IDs, not duplicated entity objects.
- Reconcile selected IDs against current query data in a colocated hook.
- Use
useRouteContext to derive selected organization, project, config, and environment from route params, search params, and React Query data.
- Use search params for route-owned selection that should survive reloads or shared links, such as SDK key config/environment and audit-log project scope.
- Use focused ID-selection hooks such as
useCollectionSelection for page-local collection selection that does not need to be linkable.
- Move repeated, context-independent client hooks to
src/core/hooks/<hook>.ts and import them directly from that file.
- Keep state reset rules near the state owner.
- Keep permission gates derived from route-context roles and shared permission helpers; client permission gates are UX only.
Never
- Do not manually patch copied server arrays in components unless optimistic UI is explicitly required.
- Do not copy React Query data into Zustand or local state just to pass it to children.
- Do not import query keys into components; invalidation belongs in API mutation hooks.
- Do not use React Context for mutable state. Context is only for stable values that do not change during app lifetime.
- Do not add Zustand unless state is truly global, cross-feature, or cross-route and other ownership options are insufficient.
- Do not use Zustand as an event bus for mutation results.
Ownership Order
- React Query hooks under
src/api/<domain> for server/cache state.
- React Router params or search params for route/navigation state.
useRouteContext for selected route resources derived from navigation state plus server data.
- Nearest common page component plus focused hooks for page workflow state.
- Local
useState or React Hook Form state for component-only state.
src/core/hooks/<hook>.ts only for repeated hooks that are independent of page/domain context.
- Small domain-specific Zustand store only for cross-route client state with no server backing.
- React Context only for stable constants or immutable services.
Reference: ai/rules/client-api-hook-rules.md
Client API Hook Rules
Rules for client API operations in apps/client/src/api.
Always
- Group API code by domain and operation under
apps/client/src/api.
- Split every operation into a request function, a React Query hook, and an operation
index.ts.
- Keep request functions free of React imports.
- Keep request functions typed with the response type they return.
- Use shared request helpers from
src/api/client.ts so private API calls share the /api/v1 base URL, JSON handling, API error handling, and credentials: "include" behavior.
- Use
useQuery or useInfiniteQuery in query hooks according to the API shape, and useMutation in mutation hooks.
- Keep query keys stable in a domain-level
queryKeys.ts when hooks or mutations share them.
- Keep object-valued query-key inputs stable and serializable, such as applied filter DTOs.
- Use
enabled in query hooks when required IDs or inputs are unavailable.
- Invalidate affected query keys inside mutation hooks.
- Import other domain query keys inside mutation hooks when the mutation makes cross-domain server state stale.
- Use explicit named exports in UI-facing barrels.
Never
- Do not call raw request functions from components.
- Do not expose request functions from operation or domain barrels used by UI.
- Do not import domain
queryKeys into components.
- Do not manually synchronize server lists in components after mutations.
- Do not mirror query data into Zustand or local state unless a concrete UI-only draft workflow requires it.
- Do not create a central
src/api/queryKeys.ts unless data is genuinely cross-domain.
- Do not use
fetch directly outside src/api/client.ts unless the request is intentionally outside the app API contract.
Layout
- Request file:
apps/client/src/api/<domain>/<operation>/<operation>.ts.
- Hook file:
apps/client/src/api/<domain>/<operation>/use<Operation>.ts.
- Operation barrel:
apps/client/src/api/<domain>/<operation>/index.ts.
- Domain barrel:
apps/client/src/api/<domain>/index.ts.
- Query keys:
apps/client/src/api/<domain>/queryKeys.ts.
Reference: ai/architecture/client-app.md
Client App Architecture
apps/client is a Vite React application for the Capture Flag platform UI.
Entry And Routing
src/main.tsx owns top-level providers.
src/router.tsx owns React Router route definitions.
- Route modules are lazy-loaded through the local
lazyRoute() helper and should expose named exports.
src/layouts contains route layout wrappers that render shared shells, navigation, headers, and nested <Outlet /> regions.
src/pages contains route-level screens. Multi-file route screens use folder modules with index.ts named exports; simple one-file screens may stay as direct page files.
src/components contains shared UI used by multiple pages or sections.
src/core contains context-independent client utilities and reusable hooks organized by category.
src/api contains client request functions, React Query hooks, operation barrels, domain barrels, and domain query keys.
src/routing contains route path and route context helpers shared by pages and layouts.
src/stories contains shared Storybook fixtures and API mocks, not component stories.
src/test contains shared Vitest and Testing Library helpers.
PlatformLayout owns the authenticated shell, navigation frame, sidebar/header state, logout flow, and nested route outlet.
- Selected organization, project, config, and environment state is derived by route helpers such as
useRouteContext, not stored in a mutable layout context.
Route Map
/: redirects to /organizations.
/login: GitHub login screen.
/account: authenticated user account details and display name editing.
/organizations and /organizations/:organizationId: organization selection and organization members.
/organizations/:organizationId/projects and /organizations/:organizationId/projects/:projectId: project selection and project members.
/organizations/:organizationId/projects/:projectId/environments: environments for the selected project.
/organizations/:organizationId/projects/:projectId/configs and /organizations/:organizationId/projects/:projectId/configs/:configId: configs and public Config JSON preview.
/organizations/:organizationId/projects/:projectId/configs/:configId/flags: feature flags and remote config values.
/organizations/:organizationId/projects/:projectId/configs/:configId/segments: reusable targeting segments.
/organizations/:organizationId/projects/:projectId/sdk-keys: SDK key lifecycle for project configs/environments, with selected config/environment in ?configId= and ?environmentId= when needed.
/organizations/:organizationId/audit-logs: organization/project audit log timeline, with selected project in ?projectId= when needed.
*: redirects to /.
Data Flow
- React Query owns server state such as authenticated user, organizations, projects, configs, environments, SDK keys, members, and feature flags.
- API operations live under
src/api/<domain>/<operation>.
- Request functions perform HTTP calls and contain no React imports.
- Query and mutation hooks are the UI-facing API.
- Query hooks may use
useQuery or useInfiniteQuery according to the API shape.
- Mutation hooks invalidate affected query keys.
- Mutations that affect derived server state may invalidate query keys from multiple API domains inside the mutation hook.
- API request and hook tests mock successful responses and API errors instead of reaching a real backend.
- Route params, search params, and server state are combined by
useRouteContext for selected resources and redirect-safe navigation paths.
- Permission gates in the client are UX only; API guards and services remain authoritative.
UI Composition
- Route components compose page sections and own screen-level flow.
- Repeated panels, forms, controls, lists, and empty states move into named components.
- React component files should stay at or below 400 lines by splitting real responsibilities into focused files.
- Short, fixed navigation or action sets should be rendered explicitly instead of through artificial arrays.
- Page-specific components stay colocated under the page folder until reused elsewhere.
- Layout-specific components stay colocated under
src/layouts/<LayoutName> until reused by another layout or page.
- Shared primitives live under
src/components and are imported directly through aliases such as @components/Button.
- Member management uses shared
components/members primitives with page-specific role options.
- Feature flag and segment page internals stay colocated under their page folders until reused.
- Storybook stories live in a
stories/ child folder beside the source area they cover.
- Route-level and cross-page grouping stories live in
src/pages/stories.
- Shared Storybook data and fetch mocks live in
src/stories and are reused by stories and page tests.
Shared Core Utilities
- Context-independent helpers and reusable client hooks live under
src/core/<category>/<name>.ts.
- Current core categories include
date, json, strings, validation, and hooks.
- Each core file exports one function or hook; import it from the direct file path such as
src/core/date/toDate.
- Do not add
index.ts barrels under src/core; multiple helpers require multiple explicit imports.
- Core tests live under
src/core/<category>/__tests__/<name>.test.ts next to the category they cover.
- Client tests live in
__tests__/ child folders beside the source area they cover, mirroring Storybook stories/ folders.
- Shared test setup and helpers live under
src/test and provide Testing Library render helpers, React Query providers, and fetch response mocks.
- Coverage is run with
npm --workspace @capture-flag/client run test:coverage and should stay at 90% or higher for configured client targets.
- Page, domain, API, or route-specific helpers stay colocated with their owning feature until they become context-independent reuse.
Client Test And Story Layout
- API request and hook tests live in
src/api/__tests__ and cover operation behavior through mocked fetch responses.
- Shared component tests live in
src/components/__tests__; member component tests live in src/components/members/__tests__.
- Page tests live in
src/pages/<PageName>/__tests__ when they cover one page folder.
- Page subsection tests live in
src/pages/<PageName>/<section>/__tests__ when they cover colocated page internals such as feature flags or segments.
- Root-level client tests live in
src/__tests__ only for source files owned directly by src, such as permissions.ts.
- Shared component stories live in
src/components/stories; member component stories live in src/components/members/stories.
- Layout stories live in
src/layouts/<LayoutName>/stories.
- Page and page subsection stories live in the owning page folder's
stories/ child folder.
- Cross-page route or panel grouping stories live in
src/pages/stories and use route parameters plus shared mock data to render realistic page states.
- Do not place component stories in
src/stories; that folder is reserved for shared Storybook data, routes, and API mocks.
Form Flow
- React Hook Form owns field state and submission.
- Zod schemas parse and validate form values.
- API mutation hooks submit normalized payloads and refresh server state.
Reference: ai/examples/good-client-api-operation.md
Good Client API Operation
Source: apps/client/src/api/projects/createProject/createProject.ts (sha256: 5510aadbf7212a2153d02de001b7211e3227050b9833b9d74f46ea522767a38e)
Source: apps/client/src/api/projects/createProject/useCreateProject.ts (sha256: 82695169e47aa65b9090e27d5acebcb91b89d5326f3bdcbffe700de16cc74af6)
Source: apps/client/src/api/projects/createProject/index.ts (sha256: 50c27c5e6cf5c4a9b979201760aa652d67d5f56785a3ea567f803cc56f3a7a4c)
Source: apps/client/src/api/auditLogs/getAuditLogs/useGetAuditLogs.ts (sha256: 5f97a43166e57aaab8312398364a5dc16754fb210fc4f2f7100a961fb6320588)
Source: apps/client/src/api/featureFlags/updateFeatureFlagEnvironmentValue/useUpdateFeatureFlagEnvironmentValue.ts (sha256: bfba73c369ba938273edf033880d189a0bea8c459cc69d445ce2ebec70cef741)
Why this is canonical:
- Splits the raw request from the React Query mutation hook.
- Keeps cache invalidation inside the mutation hook.
- Exposes hooks through operation and domain barrels instead of exposing request functions to UI code.
- Uses
useInfiniteQuery for cursor-paginated APIs.
- Keeps cross-domain cache invalidation inside mutation hooks when derived server state changes.
Canonical pattern from apps/client/src/api/projects/createProject.
Request Function
export function createProject({ name, organizationId }: CreateProjectInput) {
return postJson<Project>(`/organizations/${organizationId}/projects`, { name });
}
The request function performs HTTP work only. It does not import React.
Mutation Hook
export function useCreateProject({ organizationId, onSuccess }: UseCreateProjectOptions) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (name: string) => createProject({ name, organizationId }),
onSuccess: (project) => {
void queryClient.invalidateQueries({ queryKey: projectQueryKeys.list(organizationId) });
onSuccess?.(project);
},
});
}
The mutation hook owns cache invalidation. Components consume the hook, not the request function or query keys.
Operation Barrel
export { useCreateProject } from "./useCreateProject";
Operation and domain barrels expose hooks to UI code.
Infinite Query Hook
export function useGetAuditLogs({ enabled = true, filters, organizationId }: UseGetAuditLogsInput) {
return useInfiniteQuery({
enabled: Boolean(enabled && organizationId),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: null as string | null,
queryFn: ({ pageParam }) =>
getAuditLogs({
filters: {
...filters,
...(pageParam ? { cursor: pageParam } : {}),
},
organizationId,
}),
queryKey: auditLogQueryKeys.list(organizationId, filters),
});
}
Infinite query hooks still keep raw HTTP work in the request function and use serializable filter DTOs in query keys.
Cross-Domain Invalidation
onSuccess: (_value, variables) => {
void queryClient.invalidateQueries({ queryKey: featureFlagQueryKeys.list(configId) });
void queryClient.invalidateQueries({
queryKey: featureFlagQueryKeys.activity(configId, variables.featureFlagId),
});
void queryClient.invalidateQueries({
queryKey: configQueryKeys.preview(configId, variables.environmentId),
});
void queryClient.invalidateQueries({ queryKey: auditLogQueryKeys.all });
};
When a mutation changes derived server state, the mutation hook imports and invalidates every affected domain query key. Components still do not import query keys.