State Management

State Management Architecture 
 
 Project: {{PROJECT_NAME}}
 Version: {{VERSION}}
 Date: {{DATE}}
 Author: {{AUTHOR}}
 Status: Draft | In Review | Approved
 Reviewers: {{REVIEWERS}} 
 
 Document History 
 
 
 
 Version 
 Date 
 Author 
 Changes 
 
 
 
 
 0.1 
 {{DATE}} 
 {{AUTHOR}} 
 Initial draft 
 
 
 
 
 1. State Architecture Overview 

 {{PROJECT_NAME}} uses a layered state management approach: 
 
 
 
 Layer 
 Library 
 Scope 
 
 
 
 
 Server state 
 {{TanStack Query / SWR / Apollo}} 
 API data, caching, synchronization 
 
 
 Client / UI state 
 {{Zustand / Redux Toolkit / Pinia}} 
 Application-wide UI state 
 
 
 URL state 
 Native router APIs 
 Filters, pagination, search 
 
 
 Form state 
 {{React Hook Form / Formik / VeeValidate}} 
 Form data, validation 
 
 
 Persistent state 
 {{localStorage / cookies}} 
 User preferences, tokens 
 
 
 
 Guiding principle: Server state is NOT stored in client state. API data lives in the query cache — client state holds only UI concerns (sidebar open, selected theme, modal visibility). 
 
 2. Data Flow Diagram 

 flowchart TD
 User["User Interaction"] --> Component["Component"]

 Component -->|"API call"| QueryCache["Query Cache\n(TanStack Query)"]
 Component -->|"UI action"| ClientStore["Client Store\n(Zustand)"]
 Component -->|"Navigate"| URLState["URL State\n(Router)"]
 Component -->|"Form input"| FormState["Form State\n(RHF)"]

 QueryCache -->|"fetch / mutation"| API["Backend API"]
 QueryCache -->|"cached data"| Component
 ClientStore -->|"state slice"| Component
 URLState -->|"params"| Component
 FormState -->|"values / errors"| Component

 ClientStore -->|"user prefs"| LocalStorage["localStorage\n(Persistent)"]
 LocalStorage -->|"hydrate on load"| ClientStore
 
 
 3. State Categories 
 3.1 Server State (API Data) 

 Library: {{TanStack Query v5}} 
 Configuration: 
 const queryClient = new QueryClient({
 defaultOptions: {
 queries: {
 staleTime: 1000 * 60 * 5, // 5 minutes
 gcTime: 1000 * 60 * 30, // 30 minutes garbage collection
 retry: 2,
 refetchOnWindowFocus: true,
 refetchOnReconnect: true,
 },
 mutations: {
 retry: 0,
 },
 },
});
 
 Query key convention: 
 // Hierarchical keys for precise invalidation
['users'] // all user queries
['users', { page: 1, search: '' }] // paginated user list
['users', userId] // single user
['users', userId, 'posts'] // user's posts
 
 Stale time per resource type: 
 
 
 
 Resource 
 Stale Time 
 GC Time 
 Rationale 
 
 
 
 
 User profile 
 5 min 
 30 min 
 Changes infrequently 
 
 
 Dashboard stats 
 30 sec 
 5 min 
 Near-realtime 
 
 
 Config / enums 
 1 hour 
 24 hours 
 Rarely changes 
 
 
 Notifications 
 0 (always fresh) 
 2 min 
 Time-sensitive 
 
 
 
 
 3.2 Client State (UI State) 

 Library: {{Zustand}} 
 Store slices: 
 
 
 
 Slice 
 File 
 Responsibility 
 
 
 
 
 uiStore 
 src/stores/ui.store.ts 
 Sidebar open, active modal, theme 
 
 
 authStore 
 src/stores/auth.store.ts 
 Current user, roles, session token 
 
 
 notificationStore 
 src/stores/notification.store.ts 
 Toast queue, unread count 
 
 
 {{featureStore}} 
 src/stores/{{feature}}.store.ts 
 {{Feature-specific UI state}} 
 
 
 
 Slice template: 
 // src/stores/ui.store.ts
import { create } from 'zustand';

interface UIState {
 sidebarOpen: boolean;
 activeModal: string | null;
 theme: 'light' | 'dark' | 'system';
}

interface UIActions {
 setSidebarOpen: (open: boolean) => void;
 openModal: (id: string) => void;
 closeModal: () => void;
 setTheme: (theme: UIState['theme']) => void;
}

export const useUIStore = create<UIState & UIActions>((set) => ({
 sidebarOpen: true,
 activeModal: null,
 theme: 'system',

 setSidebarOpen: (open) => set({ sidebarOpen: open }),
 openModal: (id) => set({ activeModal: id }),
 closeModal: () => set({ activeModal: null }),
 setTheme: (theme) => set({ theme }),
}));
 
 
 3.3 URL State 

 URL state is the source of truth for: 
 
 Search / filter queries 
 Pagination (page, pageSize) 
 Sort column and direction 
 Active tab / view mode 
 Modal ID (when deep-linkable) 
 
 Convention: 
 /users?page=2&pageSize=25&search=john&sort=name&dir=asc&status=active
 
 Library: Native URLSearchParams + router useSearchParams hook 
 Serialization helper: src/lib/url-state.ts 
 // Type-safe URL param parsing with fallbacks
export function parseListParams(params: URLSearchParams): ListParams {
 return {
 page: Number(params.get('page') ?? 1),
 pageSize: Number(params.get('pageSize') ?? 25),
 search: params.get('search') ?? '',
 sort: params.get('sort') ?? 'createdAt',
 dir: (params.get('dir') as 'asc' | 'desc') ?? 'desc',
 };
}
 
 
 3.4 Form State 

 Library: {{React Hook Form v7}} 
 Validation: {{Zod}} 
 Pattern: 
 const schema = z.object({
 email: z.string().email('Invalid email'),
 name: z.string().min(2, 'Name must be at least 2 characters'),
});

const form = useForm<z.infer<typeof schema>>({
 resolver: zodResolver(schema),
 defaultValues: { email: '', name: '' },
});
 
 Rules: 
 
 Form state NEVER leaks into global store 
 Schema validation lives in src/schemas/ — reused for API validation 
 Complex multi-step forms use form context + Stepper component 
 
 
 3.5 Persistent State 

 
 
 
 Data 
 Storage 
 Library 
 Encryption 
 
 
 
 
 Theme preference 
 localStorage 
 Zustand persist middleware 
 No 
 
 
 Sidebar collapsed 
 localStorage 
 Zustand persist middleware 
 No 
 
 
 Language preference 
 localStorage 
 Native 
 No 
 
 
 Auth token 
 httpOnly cookie 
 Server-set 
 Yes (TLS) 
 
 
 Refresh token 
 httpOnly cookie 
 Server-set 
 Yes (TLS) 
 
 
 
 RULE: Auth tokens NEVER in localStorage . HttpOnly cookies only. 
 Zustand persistence example: 
 import { persist } from 'zustand/middleware';

export const usePrefsStore = create(
 persist<PrefsState>(
 (set) => ({ theme: 'system', /* ... */ }),
 { name: 'user-preferences' }
 )
);
 
 
 4. Caching Strategy 

 4.1 Optimistic Updates 
 // Optimistic update pattern with rollback
const mutation = useMutation({
 mutationFn: updateUser,
 onMutate: async (newData) => {
 // Cancel outgoing refetches
 await queryClient.cancelQueries({ queryKey: ['users', userId] });
 // Snapshot current state for rollback
 const snapshot = queryClient.getQueryData(['users', userId]);
 // Optimistically update cache
 queryClient.setQueryData(['users', userId], (old) => ({ ...old, ...newData }));
 return { snapshot };
 },
 onError: (err, vars, context) => {
 // Rollback on error
 queryClient.setQueryData(['users', userId], context?.snapshot);
 },
 onSettled: () => {
 // Always refetch to sync with server
 queryClient.invalidateQueries({ queryKey: ['users', userId] });
 },
});
 
 4.2 Cache Invalidation Rules 
 
 
 
 Mutation 
 Invalidates 
 
 
 
 
 Create user 
 ['users'] (list) 
 
 
 Update user 
 ['users', userId] 
 
 
 Delete user 
 ['users'] (list) 
 
 
 Update user role 
 ['users', userId] , ['permissions'] 
 
 
 
 
 5. Real-Time State 

 Protocol: {{WebSocket | Server-Sent Events | None}} 
 Library: {{Socket.io | native WebSocket | @microsoft/signalr}} 
 Pattern — WebSocket to Query Cache: 
 // On incoming WS event, update query cache directly
socket.on('user.updated', (user: User) => {
 queryClient.setQueryData(['users', user.id], user);
 queryClient.invalidateQueries({ queryKey: ['users'], exact: false });
});
 
 Connection management: 
 
 Reconnect with exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s 
 Show "reconnecting" banner in UI after 5s disconnect 
 Batch updates: max 50ms batching window to prevent UI thrashing 
 
 TODO: Document specific WebSocket event schema — reference event-schema-documentation.md . 
 
 6. Hydration Strategy (SSR ↔ Client) 

 Approach: {{Dehydrate/Hydrate (TanStack Query) | getServerSideProps | prefetchQuery}} 
 // Server: prefetch and dehydrate
export async function getServerSideProps() {
 const queryClient = new QueryClient();
 await queryClient.prefetchQuery({
 queryKey: ['users'],
 queryFn: fetchUsers,
 });
 return {
 props: { dehydratedState: dehydrate(queryClient) },
 };
}

// Client: hydrate — no refetch until staleTime expires
export default function Page({ dehydratedState }) {
 return (
 <HydrationBoundary state={dehydratedState}>
 <UserList />
 </HydrationBoundary>
 );
}
 
 Rule: Prefetch all critical page data on server. No loading spinners on initial navigation. 
 
 7. State Debugging Tools 

 
 
 
 Tool 
 Usage 
 Enabled In 
 
 
 
 
 TanStack Query Devtools 
 Inspect cache, queries, mutations 
 Dev only 
 
 
 Zustand devtools middleware 
 Redux DevTools integration 
 Dev only 
 
 
 React DevTools 
 Component state tree 
 Dev only 
 
 
 Redux DevTools Extension 
 If using Redux 
 Dev only 
 
 
 
 Setup: 
 // Devtools enabled only in development
const devtools = process.env.NODE_ENV === 'development'
 ? (await import('zustand/middleware')).devtools
 : (f: any) => f;
 
 
 8. Performance Considerations 

 8.1 Selector Pattern (Zustand) 
 // BAD — subscribes to entire store, re-renders on any change
const { sidebarOpen, theme } = useUIStore();

// GOOD — subscribe to only what the component needs
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const theme = useUIStore((s) => s.theme);
 
 8.2 Memoization Rules 
 
 
 
 Scenario 
 Tool 
 When to Use 
 
 
 
 
 Expensive derived data 
 useMemo 
 Only if profiling shows issue 
 
 
 Stable callback refs 
 useCallback 
 Only if passed to memoized child 
 
 
 Stable component output 
 React.memo 
 Only if parent re-renders frequently 
 
 
 
 Rule: Do NOT pre-emptively memoize. Profile first, optimize second. 
 8.3 Query Deduplication 
 TanStack Query automatically deduplicates identical queries rendered simultaneously. No additional work needed. 
 TODO: Run React Profiler on critical paths and document findings. 
 
 Approval 
 
 
 
 Role 
 Name 
 Date 
 Signature 
 
 
 
 
 Author 
 
 
 
 
 
 Frontend Lead 
 
 
 
 
 
 Tech Lead