State Architecture Patterns & Trade-offs
📖 Concept
State management is the #1 architectural decision that affects long-term maintainability. At scale (50+ screens, 10+ engineers), the wrong choice compounds into tech debt that takes quarters to fix.
State categories (critical mental model):
| Type | Examples | Persistence | Reactivity | Tool |
|---|---|---|---|---|
| Server state | API data, user profile | Cache + API sync | Subscribe to changes | React Query / SWR |
| Client state | Form inputs, UI toggles | Session or none | Local re-render | useState / Zustand |
| Global state | Auth, theme, feature flags | Persistent | All consumers | Zustand / Redux |
| Navigation state | Current route, params | In-memory | Navigation events | React Navigation |
| Derived state | Filtered list, totals | Never (computed) | Re-compute on change | useMemo / selectors |
The mistake 90% of teams make: Putting EVERYTHING in one global store (Redux). Server state (API data) has completely different lifecycle characteristics than UI state. Mixing them creates:
- Stale data (no automatic refetch)
- Manual cache invalidation (error-prone)
- Bloated reducers with loading/error/data for every API call
- Unnecessary re-renders (updating one API cache triggers all consumers)
Modern state architecture:
Server State → React Query / TanStack Query
├── Automatic caching, refetching, invalidation
├── Stale-while-revalidate pattern
└── Optimistic updates with rollback
Client State → Zustand (or useState for local)
├── Minimal boilerplate
├── Selector-based re-renders
└── Middleware (persist, devtools, immer)
Global Config → Context (for infrequently-changing data)
├── Theme, locale, feature flags
└── Memoized provider values
💻 Code Example
1// === MODERN STATE ARCHITECTURE ===23// 1. Server State with React Query (TanStack Query)4import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';56// Query keys as constants — prevents typos and enables invalidation7const queryKeys = {8 users: ['users'] as const,9 user: (id: string) => ['users', id] as const,10 posts: (filters?: PostFilters) => ['posts', filters] as const,11 post: (id: string) => ['posts', id] as const,12};1314// Custom hook for user data15function useUser(userId: string) {16 return useQuery({17 queryKey: queryKeys.user(userId),18 queryFn: () => api.getUser(userId),19 staleTime: 5 * 60 * 1000, // 5 min — don't refetch if fresh20 gcTime: 30 * 60 * 1000, // 30 min — keep in cache21 retry: 3, // Auto-retry on failure22 });23}2425// Mutation with optimistic update26function useUpdateProfile() {27 const queryClient = useQueryClient();2829 return useMutation({30 mutationFn: (data: UpdateProfileDTO) => api.updateProfile(data),3132 // Optimistic update — show change immediately, rollback on error33 onMutate: async (newData) => {34 await queryClient.cancelQueries({ queryKey: queryKeys.user('me') });3536 const previousUser = queryClient.getQueryData(queryKeys.user('me'));3738 queryClient.setQueryData(queryKeys.user('me'), (old: User) => ({39 ...old,40 ...newData,41 }));4243 return { previousUser }; // Context for rollback44 },4546 onError: (_err, _newData, context) => {47 // Rollback on error48 queryClient.setQueryData(queryKeys.user('me'), context?.previousUser);49 },5051 onSettled: () => {52 // Refetch in background to ensure consistency53 queryClient.invalidateQueries({ queryKey: queryKeys.user('me') });54 },55 });56}5758// 2. Client State with Zustand59import { create } from 'zustand';60import { persist, createJSONStorage } from 'zustand/middleware';61import AsyncStorage from '@react-native-async-storage/async-storage';6263interface AppState {64 // UI State65 isDarkMode: boolean;66 selectedTab: 'home' | 'search' | 'profile';6768 // Session State69 isOnboarded: boolean;70 lastViewedProductId: string | null;7172 // Actions73 toggleDarkMode: () => void;74 setTab: (tab: AppState['selectedTab']) => void;75 markOnboarded: () => void;76}7778const useAppStore = create<AppState>()(79 persist(80 (set) => ({81 isDarkMode: false,82 selectedTab: 'home',83 isOnboarded: false,84 lastViewedProductId: null,8586 toggleDarkMode: () => set((s) => ({ isDarkMode: !s.isDarkMode })),87 setTab: (tab) => set({ selectedTab: tab }),88 markOnboarded: () => set({ isOnboarded: true }),89 }),90 {91 name: 'app-state',92 storage: createJSONStorage(() => AsyncStorage),93 partialize: (state) => ({94 // Only persist these fields95 isDarkMode: state.isDarkMode,96 isOnboarded: state.isOnboarded,97 }),98 }99 )100);101102// Selector-based subscriptions — only re-renders when selected value changes103function ThemeToggle() {104 const isDarkMode = useAppStore(s => s.isDarkMode);105 const toggle = useAppStore(s => s.toggleDarkMode);106 // This component ONLY re-renders when isDarkMode changes107 // Not when selectedTab or anything else changes108 return <Switch value={isDarkMode} onValueChange={toggle} />;109}
🏋️ Practice Exercise
State Architecture Exercises:
- Audit your current app's state — categorize every piece of state into server/client/global/navigation/derived
- Migrate one API-fetching Redux slice to React Query and compare the code reduction
- Implement a Zustand store with persistence (AsyncStorage/MMKV) and selective persistence
- Build an optimistic update flow: edit a todo → show change immediately → rollback on API error
- Create a selector-based Zustand store and verify that only subscribed components re-render
- Implement a cache invalidation strategy for interrelated resources (e.g., updating a post should invalidate the feed)
⚠️ Common Mistakes
Storing server state in Redux with manual loading/error tracking — React Query handles this automatically with better caching and invalidation
Not distinguishing between state categories — leads to a monolithic store that's hard to reason about and causes unnecessary re-renders
Using Context for frequently-changing state — Context re-renders ALL consumers on any change, even with useMemo on the value
Not normalizing state — storing nested/duplicated data wastes memory and makes updates error-prone
Over-centralizing — putting form state in a global store when it should be local to the component
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for State Architecture Patterns & Trade-offs. Login to unlock this feature.