# 🎭 State Management Consolidation Guide **Document Date:** December 25, 2025 **Purpose:** Unified strategy for scattered state across components, context, and database **Target:** Consistent, testable, debuggable state patterns --- ## 🎯 Current Situation ### State Scatter Map ``` Component State (useState) [LOCAL] ├─ UI state (expanded, focused) ├─ Form inputs ├─ Modal open/closed └─ Temporary data Context API [GLOBAL] ├─ Auth context ├─ Theme context ├─ User settings └─ Notifications Database State (Prisma) [PERSISTENT] ├─ User data ├─ Configuration ├─ Preferences └─ Audit logs Third-party Libraries [EXTERNAL] ├─ React Query (caching) ├─ React Hook Form (forms) ├─ Redux (if used) └─ Zustand (if used) ``` **Problems:** 1. ❌ No clear decision criteria → inconsistent choices 2. ❌ Overlapping patterns → duplicate state 3. ❌ No caching strategy → unnecessary database calls 4. ❌ Context sprawl → 10+ providers 5. ❌ No clear data flow → hard to debug --- ## ✅ Unified State Architecture ### 4 State Categories ``` ┌─────────────────────────────────────────────────────────┐ │ STATE MANAGEMENT HIERARCHY │ ├─────────────────────────────────────────────────────────┤ │ │ │ CATEGORY 1: LOCAL STATE (useState) │ │ Duration: Session │ │ Scope: Single component │ │ Persistence: None │ │ Max items: 2-3 per component │ │ ✓ UI toggles, temporary form data │ │ │ ├─────────────────────────────────────────────────────────┤ │ │ │ CATEGORY 2: GLOBAL STATE (Context) │ │ Duration: Session │ │ Scope: Entire app │ │ Persistence: Session storage │ │ Max providers: 3-4 │ │ ✓ Auth, theme, notifications │ │ │ ├─────────────────────────────────────────────────────────┤ │ │ │ CATEGORY 3: DATABASE STATE (Prisma + React) │ │ Duration: Permanent │ │ Scope: Multi-session │ │ Persistence: Database │ │ ✓ User data, configs, preferences │ │ │ ├─────────────────────────────────────────────────────────┤ │ │ │ CATEGORY 4: CACHE STATE (React Query) │ │ Duration: Temporary with invalidation │ │ Scope: Single or multiple components │ │ Persistence: Memory │ │ ✓ API responses, expensive queries │ │ │ └─────────────────────────────────────────────────────────┘ ``` --- ## 📋 Decision Tree ``` START: I need to store some data Is this data user-specific or tenant-specific? ├─ NO → Is it needed globally across the app? │ ├─ NO → LOCAL STATE (useState) │ │ └─ Keep it simple, short-lived │ └─ YES → Is it frequently accessed? │ ├─ YES → GLOBAL STATE (Context) │ │ └─ Auth, theme, settings │ └─ NO → CACHE STATE (React Query) │ └─ Fetch once, reuse │ └─ YES → Will it outlive the current session? ├─ NO → LOCAL STATE or CACHE │ └─ Temporary user interactions └─ YES → DATABASE STATE (Prisma) └─ Save to DB, load on app start ``` --- ## 1️⃣ LOCAL STATE: useState ### Definition Ephemeral state that lives only during current component render cycle. ### When to Use ```typescript // ✅ DO: Use for UI state const [isExpanded, setIsExpanded] = useState(false); const [activeTab, setActiveTab] = useState(0); const [hoverIndex, setHoverIndex] = useState(-1); // ✅ DO: Use for form inputs before submission const [firstName, setFirstName] = useState(''); const [email, setEmail] = useState(''); // ✅ DO: Use for temporary data const [searchResults, setSearchResults] = useState([]); // ❌ DON'T: Use for data that should persist const [userPreferences, setUserPreferences] = useState({}); // → Move to DATABASE STATE // ❌ DON'T: Use for app-wide settings const [isDarkMode, setIsDarkMode] = useState(false); // → Move to GLOBAL STATE or DATABASE STATE // ❌ DON'T: Duplicate server state locally const [userData, setUserData] = useState(null); // → Use CACHE STATE with React Query ``` ### Patterns **Simple Toggle:** ```typescript export function AccordionItem({ title, children }) { const [isOpen, setIsOpen] = useState(false); return (
{isOpen &&
{children}
}
); } ``` **Form Inputs:** ```typescript export function UserForm({ onSubmit }) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); return (
{ e.preventDefault(); onSubmit({ name, email }); setName(''); // Clear after submit setEmail(''); }}> setName(e.target.value)} /> setEmail(e.target.value)} />
); } ``` **Multiple Related States (Use Object):** ```typescript export function Modal() { const [state, setState] = useState({ isOpen: false, position: { x: 0, y: 0 }, size: { width: 500, height: 300 }, }); const setIsOpen = (isOpen) => setState((s) => ({ ...s, isOpen })); const setPosition = (position) => setState((s) => ({ ...s, position })); // ... use state.isOpen, setIsOpen, setPosition } ``` ### Limits - **Max per component:** 2-3 items - **Max data size:** < 1 KB - **Duration:** Current session only - **Scope:** Single component only --- ## 2️⃣ GLOBAL STATE: Context API ### Definition Application-wide state shared across many components without prop drilling. ### When to Use ```typescript // ✅ DO: Authentication context const AuthContext = createContext(); // → User data, tokens, login/logout functions // ✅ DO: Theme context const ThemeContext = createContext(); // → Dark/light mode, color scheme // ✅ DO: Notifications context const NotificationsContext = createContext(); // → Toast/alert messages, dismissal // ❌ DON'T: Component-specific state // → Keep in component with useState // ❌ DON'T: Frequently changing data // → Use CACHE STATE instead, Context updates = expensive re-renders // ❌ DON'T: Server state // → Use CACHE STATE with React Query ``` ### Maximum 3-4 Global Contexts ``` ┌─ AuthContext │ ├─ user │ ├─ isAuthenticated │ └─ login/logout ├─ ThemeContext │ ├─ isDarkMode │ └─ setDarkMode ├─ NotificationsContext │ ├─ notifications[] │ ├─ addNotification │ └─ removeNotification └─ (Optional) SettingsContext ├─ userSettings └─ updateSettings ``` **Never add:** - ❌ Global component visibility state - ❌ Global form state - ❌ Global selection state - ❌ Global search state --- ### Pattern: AuthContext ```typescript // src/context/AuthContext.tsx import { createContext, useContext, useState, useEffect } from 'react'; interface AuthContextType { user: User | null; isAuthenticated: boolean; isLoading: boolean; login: (email: string, password: string) => Promise; logout: () => void; } const AuthContext = createContext(undefined); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); // Restore user from session storage on mount useEffect(() => { const stored = sessionStorage.getItem('user'); if (stored) { setUser(JSON.parse(stored)); } setIsLoading(false); }, []); const login = async (email: string, password: string) => { setIsLoading(true); try { const response = await api.post('/auth/login', { email, password }); setUser(response.user); sessionStorage.setItem('user', JSON.stringify(response.user)); } finally { setIsLoading(false); } }; const logout = () => { setUser(null); sessionStorage.removeItem('user'); }; return ( {children} ); } // Custom hook for using auth export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within AuthProvider'); } return context; } ``` **Usage in components:** ```typescript export function Header() { const { user, isAuthenticated, logout } = useAuth(); if (!isAuthenticated) { return
Not logged in
; } return (
Welcome, {user?.name}
); } ``` --- ## 3️⃣ DATABASE STATE: Prisma + React State ### Definition Persistent data stored in database, loaded into React state, cached during session. ### When to Use ```typescript // ✅ DO: User preferences const [userPrefs, setUserPrefs] = useState(null); useEffect(() => { Database.getUserPreferences(userId).then(setUserPrefs); }, [userId]); // ✅ DO: Configuration data const [config, setConfig] = useState(null); useEffect(() => { Database.getConfig(tenantId).then(setConfig); }, [tenantId]); // ✅ DO: Lists of items from database const [items, setItems] = useState([]); useEffect(() => { Database.getItems(tenantId).then(setItems); }, [tenantId]); // ❌ DON'T: Cache without React Query // → Too complex to manage updates/refetch // ❌ DON'T: Forget tenantId in queries const items = await Database.getItems(); // ❌ WRONG const items = await Database.getItems({ tenantId }); // ✅ RIGHT ``` ### Pattern: Database State Hook ```typescript // src/hooks/useUserPreferences.ts export interface UserPreferences { theme: 'light' | 'dark'; language: string; notifications: boolean; } export function useUserPreferences(userId: string) { const [prefs, setPrefs] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Load preferences on mount useEffect(() => { const load = async () => { try { setLoading(true); const data = await Database.getUserPreferences(userId); setPrefs(data); } catch (err) { setError(err as Error); } finally { setLoading(false); } }; load(); }, [userId]); // Update preferences (write-through cache) const updatePrefs = useCallback( async (updates: Partial) => { try { const updated = await Database.updateUserPreferences(userId, updates); setPrefs(updated); } catch (err) { setError(err as Error); // Revert optimistic update if it fails const current = await Database.getUserPreferences(userId); setPrefs(current); } }, [userId] ); return { prefs, loading, error, updatePrefs }; } ``` **Usage:** ```typescript export function SettingsPanel() { const { user } = useAuth(); const { prefs, loading, updatePrefs } = useUserPreferences(user.id); if (loading) return
Loading...
; return (
); } ``` ### Database Schema Example ```prisma // prisma/schema.prisma model UserPreferences { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id]) tenantId String // Preferences data theme String @default("light") // 'light' | 'dark' language String @default("en") notifications Boolean @default(true) sidebarWidth Int @default(250) // Metadata createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, tenantId]) } ``` --- ## 4️⃣ CACHE STATE: React Query ### Definition Temporary cached state for API/database queries with automatic invalidation. ### When to Use ```typescript // ✅ DO: API responses const { data: workflows } = useQuery({ queryKey: ['workflows', userId], queryFn: () => api.get(`/workflows?user=${userId}`), }); // ✅ DO: Expensive database queries const { data: analytics } = useQuery({ queryKey: ['analytics', dateRange], queryFn: () => Database.getAnalytics(dateRange), }); // ✅ DO: Frequently changing data const { data: notifications } = useQuery({ queryKey: ['notifications'], queryFn: () => api.get('/notifications'), refetchInterval: 5000, // Poll every 5 seconds }); // ❌ DON'T: Use for always-fresh data // → Use state + useEffect instead // ❌ DON'T: Forget queryKey const { data } = useQuery({ queryFn: () => fetch('/api/data'), // ❌ No queryKey means no caching }); ``` ### Pattern: React Query Hook ```typescript // src/hooks/useWorkflows.ts import { useQuery } from '@tanstack/react-query'; export function useWorkflows(userId: string) { return useQuery({ queryKey: ['workflows', userId], queryFn: async () => { const response = await Database.getWorkflows({ userId }); return response; }, staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 10, // 10 minutes cache }); } export function useWorkflowDetails(workflowId: string) { return useQuery({ queryKey: ['workflow', workflowId], queryFn: async () => { return Database.getWorkflow(workflowId); }, enabled: !!workflowId, // Don't fetch if no ID }); } export function useUpdateWorkflow() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: UpdateWorkflowData) => Database.updateWorkflow(data), onSuccess: (newData) => { // Invalidate related queries queryClient.invalidateQueries({ queryKey: ['workflows'], }); // Update specific query queryClient.setQueryData(['workflow', newData.id], newData); }, }); } ``` **Usage:** ```typescript export function WorkflowsList() { const { data: workflows, isLoading, error } = useWorkflows('user-123'); const updateMutation = useUpdateWorkflow(); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return (
{workflows?.map((workflow) => (

{workflow.name}

))}
); } ``` ### Setup QueryClient ```typescript // src/main.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute gcTime: 1000 * 60 * 10, // 10 minutes retry: 1, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, }, }); export default function App() { return ( ); } ``` --- ## 🔄 State Flow Diagram ``` ┌─ USER ACTION (click, input, etc) │ ├─ LOCAL STATE (useState) ──────────→ Update immediately │ └─ Re-render component │ ├─ GLOBAL STATE (Context) ──────────→ Update provider │ └─ Re-render all consumers │ ├─ DATABASE STATE (Prisma + useState) │ ├─ Read: Load on mount │ │ └─ useState → Re-render │ └─ Write: Save to DB │ ├─ Optimistic update to state │ ├─ Database operation │ └─ Invalidate cache if fails │ └─ CACHE STATE (React Query) ├─ Read: useQuery │ ├─ Return cached if fresh │ └─ Fetch if stale ├─ Write: useMutation │ ├─ Optimistic update │ ├─ Server operation │ ├─ Success: invalidate queries │ └─ Error: rollback └─ Auto-invalidate: staleTime expiry ``` --- ## 📊 State Pattern Decision Matrix | Scenario | Local | Context | Database | Cache | |----------|-------|---------|----------|-------| | UI toggle | ✅ | ❌ | ❌ | ❌ | | Form input | ✅ | ❌ | ❌ | ❌ | | Current user | ❌ | ✅ | ✅ | ❌ | | App settings | ❌ | ✅ | ✅ | ❌ | | Theme | ❌ | ✅ | ✅ | ❌ | | User preferences | ❌ | ❌ | ✅ | ❌ | | API response | ❌ | ❌ | ❌ | ✅ | | Search results | ✅ | ❌ | ❌ | ✅ | | Analytics data | ❌ | ❌ | ❌ | ✅ | | List of items | ❌ | ❌ | ✅ | ✅ | | Notification | ✅ | ✅ | ❌ | ❌ | --- ## ⚠️ Anti-Patterns to Avoid ### ❌ Anti-Pattern 1: Global Context for Everything ```typescript // ❌ WRONG: Monolithic context with everything const GlobalContext = createContext(); // Contains: user, theme, notifications, modals, forms, ... // Result: Re-renders entire app on any change ❌ ``` **Fix:** Split into separate concerns ```typescript // ✅ RIGHT: Separate contexts const AuthContext = createContext(); // User info const ThemeContext = createContext(); // Theme settings const NotificationsContext = createContext(); // Alerts ``` --- ### ❌ Anti-Pattern 2: Database Reads Scattered ```typescript // ❌ WRONG: Each component fetches directly function ComponentA() { const [data, setData] = useState(null); useEffect(() => { Database.getData().then(setData); }, []); } function ComponentB() { const [data, setData] = useState(null); useEffect(() => { Database.getData().then(setData); // Same query, duplicate fetch! }, []); } ``` **Fix:** Use custom hook or React Query ```typescript // ✅ RIGHT: Centralized query export function useData() { return useQuery({ queryKey: ['data'], queryFn: () => Database.getData(), }); } function ComponentA() { const { data } = useData(); // Uses cache } function ComponentB() { const { data } = useData(); // Same cached result } ``` --- ### ❌ Anti-Pattern 3: Forgetting tenantId ```typescript // ❌ WRONG: No tenant isolation const items = await Database.getItems(); // This might return items from OTHER tenants! ``` **Fix:** Always include tenantId ```typescript // ✅ RIGHT: Tenant-aware query const items = await Database.getItems({ tenantId: currentUser.tenantId, }); ``` --- ### ❌ Anti-Pattern 4: useState for Server State ```typescript // ❌ WRONG: Duplicate server state locally const [user, setUser] = useState(null); useEffect(() => { Database.getUser(userId).then(setUser); }, [userId]); // Problem: Manual cache invalidation needed ❌ ``` **Fix:** Use React Query ```typescript // ✅ RIGHT: Let React Query manage cache const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => Database.getUser(userId), }); // Automatic cache management ✅ ``` --- ## 🎯 Migration Checklist ### Step 1: Audit (1 week) - [ ] List all useState instances - [ ] List all useContext instances - [ ] Map all Database reads - [ ] Identify cache opportunities ### Step 2: Define (1 week) - [ ] Choose context structure (max 3-4) - [ ] Define database models - [ ] Set up React Query - [ ] Document patterns ### Step 3: Implement (2-3 weeks) - [ ] Create standard hooks - [ ] Migrate component state - [ ] Migrate context usage - [ ] Add React Query ### Step 4: Test (1 week) - [ ] Unit tests for hooks - [ ] Integration tests - [ ] E2E tests - [ ] Performance testing ### Step 5: Deploy (Ongoing) - [ ] Gradual rollout - [ ] Monitor performance - [ ] Fix issues - [ ] Document learnings --- ## 📚 Templates ### Custom Hook Template ```typescript // src/hooks/use[Name].ts import { useCallback, useEffect, useState } from 'react'; export interface [Name]State { // Define your state shape data: any; loading: boolean; error: Error | null; } export function use[Name](/* params */) { const [state, setState] = useState<[Name]State>({ data: null, loading: false, error: null, }); // Load data on mount useEffect(() => { // Load logic }, [/* dependencies */]); // Action handlers const action = useCallback(async (/* params */) => { // Action logic }, []); return { ...state, action }; } ``` ### Context Provider Template ```typescript // src/context/[Name]Context.tsx import { createContext, useContext, useState } from 'react'; interface [Name]ContextType { // Define your context } const [Name]Context = createContext<[Name]ContextType | undefined>(undefined); export function [Name]Provider({ children }) { const [state, setState] = useState({ // Initial state }); return ( <[Name]Context.Provider value={{ state, setState }}> {children} ); } export function use[Name]() { const context = useContext([Name]Context); if (!context) { throw new Error('use[Name] must be used within [Name]Provider'); } return context; } ``` --- ## 🚀 Implementation Order 1. **Setup React Query** (4 hours) - Install packages - Create QueryClient - Set up provider - Add dev tools 2. **Create Context Structure** (6 hours) - AuthContext - ThemeContext - NotificationsContext - Custom hooks 3. **Migrate Database State** (20 hours) - Identify all database reads - Create hooks for each query - Replace scattered useState - Add React Query where applicable 4. **Consolidate Component State** (15 hours) - Audit all useState usage - Move global state to context - Move persistent state to database - Keep only UI state local 5. **Testing** (10 hours) - Unit tests for hooks - Integration tests - E2E tests --- ## ✅ Success Metrics - [ ] Max 3-4 global context providers - [ ] 80% reduction in useState instances in containers - [ ] 100% of database reads use hooks - [ ] 90%+ cache hit rate for repeated queries - [ ] Zero duplicate database fetches - [ ] All multi-tenant queries include tenantId - [ ] <100ms time to interactive - [ ] Zero console warnings/errors --- **Generated:** December 25, 2025 **Next Review:** After Phase 2 completion