Frontend Engineering

State Management Architecture in Modern React

Compare React state management solutions: Redux Toolkit, Zustand, and Context API. Learn when to use each with real examples and performance considerations.

Khalid Aboubakr
15 min read
State ManagementReduxZustandReact QueryArchitectureContext ApiRedux Toolkit

Introduction

State management in React has evolved significantly. The days of putting everything in Redux are behind us. Modern applications benefit from understanding the different types of state and using the right tool for each.

The State Quadrant

Not all state is created equal. Understanding these categories helps choose the right approach:

                    Short-lived              Long-lived
              ┌─────────────────────┬─────────────────────┐
              │                     │                     │
    Local     │   Form input state  │   Component prefs   │
              │   UI toggles        │   Draft content     │
              │   (useState)        │   (useState+storage)│
              │                     │                     │
              ├─────────────────────┼─────────────────────┤
              │                     │                     │
    Global    │   Notifications     │   User session      │
              │   Modals            │   App settings      │
              │   (Context/Zustand) │   (Zustand/Redux)   │
              │                     │                     │
              └─────────────────────┴─────────────────────┘

                    Server State (React Query / SWR)
              ┌─────────────────────────────────────────────┐
              │   Cached API responses                      │
              │   Background refetching                     │
              │   Optimistic updates                        │
              └─────────────────────────────────────────────┘

Pattern 1: Server State with React Query

Most "global state" is actually server state in disguise:

// ❌ Old pattern: fetch in useEffect, store in Redux useEffect(() => { dispatch(fetchUsers()); }, []); const users = useSelector(state => state.users.data); const loading = useSelector(state => state.users.loading); // ✅ Modern pattern: React Query handles caching, loading, errors const { data: users, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: fetchUsers, staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes });

Advanced Query Patterns

// Dependent queries const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); const { data: projects } = useQuery({ queryKey: ['projects', user?.teamId], queryFn: () => fetchTeamProjects(user!.teamId), enabled: !!user?.teamId, // Only run when we have teamId }); // Parallel queries const results = useQueries({ queries: projectIds.map(id => ({ queryKey: ['project', id], queryFn: () => fetchProject(id), })) }); // Infinite queries for pagination const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam), getNextPageParam: (lastPage) => lastPage.nextCursor, });

Pattern 2: Client State with Zustand

For truly client-side global state, Zustand offers simplicity:

import { create } from 'zustand'; import { persist, devtools } from 'zustand/middleware'; interface UIState { sidebarCollapsed: boolean; theme: 'light' | 'dark'; toggleSidebar: () => void; setTheme: (theme: 'light' | 'dark') => void; } const useUIStore = create<UIState>()( devtools( persist( (set) => ({ sidebarCollapsed: false, theme: 'light', toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), setTheme: (theme) => set({ theme }), }), { name: 'ui-store' } ) ) ); // Usage with selectors for minimal re-renders function Sidebar() { const collapsed = useUIStore((state) => state.sidebarCollapsed); const toggle = useUIStore((state) => state.toggleSidebar); // Only re-renders when collapsed changes }

Sliced Store Pattern

// Split complex stores into slices interface AuthSlice { user: User | null; login: (credentials: Credentials) => Promise<void>; logout: () => void; } interface CartSlice { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: string) => void; } const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = (set) => ({ user: null, login: async (credentials) => { const user = await authApi.login(credentials); set({ user }); }, logout: () => set({ user: null }), }); const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = (set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })), }); const useStore = create<AuthSlice & CartSlice>()((...a) => ({ ...createAuthSlice(...a), ...createCartSlice(...a), }));

Pattern 3: Form State

Forms deserve dedicated state management:

import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const userSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email address'), role: z.enum(['admin', 'user', 'guest']), }); type UserFormData = z.infer<typeof userSchema>; function UserForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, reset, } = useForm<UserFormData>({ resolver: zodResolver(userSchema), defaultValues: { name: '', email: '', role: 'user', }, }); const mutation = useMutation({ mutationFn: createUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); reset(); }, }); return ( <form onSubmit={handleSubmit((data) => mutation.mutate(data))}> <input {...register('name')} /> {errors.name && <span>{errors.name.message}</span>} <input {...register('email')} /> {errors.email && <span>{errors.email.message}</span>} <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Saving...' : 'Save'} </button> </form> ); }

Pattern 4: URL State

Some state belongs in the URL for shareability and back button support:

import { useSearchParams } from 'react-router-dom'; function ProductList() { const [searchParams, setSearchParams] = useSearchParams(); const filters = { category: searchParams.get('category') || 'all', sort: searchParams.get('sort') || 'newest', page: parseInt(searchParams.get('page') || '1'), }; const updateFilters = (updates: Partial<typeof filters>) => { setSearchParams((prev) => { Object.entries(updates).forEach(([key, value]) => { if (value) { prev.set(key, String(value)); } else { prev.delete(key); } }); return prev; }); }; const { data } = useQuery({ queryKey: ['products', filters], queryFn: () => fetchProducts(filters), }); return ( <div> <select value={filters.category} onChange={(e) => updateFilters({ category: e.target.value, page: 1 })} > {/* options */} </select> {/* Product list */} </div> ); }

Choosing the Right Tool

// Decision tree function chooseStateManagement(stateType: string) { if (stateType === 'server-data') { return 'React Query or SWR'; } if (stateType === 'form') { return 'React Hook Form or Formik'; } if (stateType === 'url-parameters') { return 'useSearchParams'; } if (stateType === 'component-local') { return 'useState or useReducer'; } if (stateType === 'shared-between-few-components') { return 'Lift state up or Context'; } if (stateType === 'global-client-state') { return 'Zustand (simple) or Redux Toolkit (complex)'; } }

Conclusion

Modern state management is about using the right tool for each type of state:

  1. Server state: React Query handles caching, refetching, and synchronization
  2. Form state: React Hook Form with validation libraries
  3. URL state: React Router's useSearchParams
  4. Global client state: Zustand for simplicity, Redux Toolkit for complexity
  5. Component state: useState and useReducer

Don't reach for global state by default. Most state is either server state (use React Query) or local state (use useState). True global client state is rarer than you think.

Related Articles

Frontend Engineering17 min read

React Performance Optimization at Scale

Optimize React performance with useMemo, useCallback, and code splitting. Real performance metrics and patterns from large-scale production applications.

Frontend Engineering16 min read

Advanced TypeScript Patterns for Large Codebases

Master advanced TypeScript patterns including generics, utility types, and type guards. Real examples from large-scale React and Node.js codebases.

Performance & SEO19 min read

Core Web Vitals Optimization: A Technical Deep Dive

Improve Core Web Vitals scores with practical LCP, FID, and CLS optimization techniques. Real before/after metrics from production React and Next.js applications.

Backend Design19 min read

API Design: Choosing Between REST, GraphQL, and gRPC

Compare REST, GraphQL, and gRPC APIs with performance benchmarks and use cases. Learn which API style fits your project based on real production experience.