Back to Blog
State ManagementReactArchitectureFrontend

State Management Patterns for Modern Applications

Navigate the state management landscape with AI guidance. From local state to global stores, learn when to use each pattern.

B
Bootspring Team
Engineering
December 6, 2025
6 min read

State management is one of the most debated topics in frontend development. With countless libraries and patterns available, choosing the right approach requires understanding your application's specific needs. AI can help you navigate these decisions.

Understanding State Categories#

Not all state is equal. Categorizing state helps choose the right management approach:

Local UI State#

  • Form input values
  • Modal open/closed
  • Dropdown selections
  • Animation states

Pattern: Component state (useState, useReducer)

Shared UI State#

  • Theme preferences
  • Sidebar collapsed
  • Active filters
  • Sort orders

Pattern: Context API or lightweight stores

Server Cache State#

  • API response data
  • User profiles
  • Product listings
  • Search results

Pattern: React Query, SWR, Apollo

Global Application State#

  • Authentication
  • User permissions
  • Shopping cart
  • Notification queue

Pattern: Redux, Zustand, Jotai

Pattern 1: Local State with Hooks#

For truly local state, hooks are sufficient:

1function SearchForm() { 2 const [query, setQuery] = useState(''); 3 const [isExpanded, setIsExpanded] = useState(false); 4 5 return ( 6 <form onSubmit={() => onSearch(query)}> 7 <input 8 value={query} 9 onChange={(e) => setQuery(e.target.value)} 10 onFocus={() => setIsExpanded(true)} 11 /> 12 {isExpanded && <FilterOptions />} 13 </form> 14 ); 15}

When to use: State doesn't need to be shared, component lifecycle matches state lifecycle.

Pattern 2: Lifting State Up#

When siblings need to share state:

1function ProductPage() { 2 const [selectedVariant, setSelectedVariant] = useState(null); 3 4 return ( 5 <div> 6 <ProductImages variant={selectedVariant} /> 7 <ProductDetails 8 selectedVariant={selectedVariant} 9 onSelectVariant={setSelectedVariant} 10 /> 11 <AddToCart variant={selectedVariant} /> 12 </div> 13 ); 14}

When to use: Small component trees, clear data flow.

Pattern 3: Context for Dependency Injection#

Context works well for data that many components need:

1const ThemeContext = createContext<Theme>('light'); 2const UserContext = createContext<User | null>(null); 3 4function App() { 5 const [theme, setTheme] = useState<Theme>('light'); 6 const [user, setUser] = useState<User | null>(null); 7 8 return ( 9 <ThemeContext.Provider value={{ theme, setTheme }}> 10 <UserContext.Provider value={user}> 11 <MainContent /> 12 </UserContext.Provider> 13 </ThemeContext.Provider> 14 ); 15} 16 17// Deeply nested component 18function UserAvatar() { 19 const user = useContext(UserContext); 20 const { theme } = useContext(ThemeContext); 21 22 return <Avatar user={user} theme={theme} />; 23}

When to use: Data accessed by many components at different levels.

Warning: Context causes re-renders of all consumers. Split contexts by update frequency.

Pattern 4: Server State with React Query#

For data that comes from APIs:

1function ProductList({ categoryId }) { 2 const { 3 data: products, 4 isLoading, 5 error, 6 refetch 7 } = useQuery({ 8 queryKey: ['products', categoryId], 9 queryFn: () => fetchProducts(categoryId), 10 staleTime: 5 * 60 * 1000, // 5 minutes 11 }); 12 13 if (isLoading) return <Skeleton />; 14 if (error) return <ErrorMessage error={error} onRetry={refetch} />; 15 16 return <ProductGrid products={products} />; 17}

Benefits:

  • Automatic caching
  • Background refetching
  • Optimistic updates
  • Request deduplication

When to use: Any data from external sources.

Pattern 5: Global Store with Zustand#

For application-wide state:

1interface CartStore { 2 items: CartItem[]; 3 addItem: (item: CartItem) => void; 4 removeItem: (itemId: string) => void; 5 clear: () => void; 6 total: () => number; 7} 8 9const useCartStore = create<CartStore>((set, get) => ({ 10 items: [], 11 12 addItem: (item) => set((state) => ({ 13 items: [...state.items, item] 14 })), 15 16 removeItem: (itemId) => set((state) => ({ 17 items: state.items.filter((i) => i.id !== itemId) 18 })), 19 20 clear: () => set({ items: [] }), 21 22 total: () => get().items.reduce((sum, item) => sum + item.price, 0), 23})); 24 25// Usage anywhere 26function CartIcon() { 27 const itemCount = useCartStore((state) => state.items.length); 28 return <Badge count={itemCount}><ShoppingCartIcon /></Badge>; 29} 30 31function CartTotal() { 32 const total = useCartStore((state) => state.total()); 33 return <span>${total.toFixed(2)}</span>; 34}

When to use: State that needs to persist across navigation, accessible from anywhere.

Pattern 6: Atomic State with Jotai#

For fine-grained reactivity:

1// Define atoms 2const filterAtom = atom<Filter>({ category: 'all', priceRange: [0, 100] }); 3const sortAtom = atom<Sort>('newest'); 4const searchAtom = atom<string>(''); 5 6// Derived atom 7const filteredProductsAtom = atom((get) => { 8 const filter = get(filterAtom); 9 const sort = get(sortAtom); 10 const search = get(searchAtom); 11 12 return products 13 .filter(matchesFilter(filter)) 14 .filter(matchesSearch(search)) 15 .sort(sortBy(sort)); 16}); 17 18// Components only re-render when their atoms change 19function FilterPanel() { 20 const [filter, setFilter] = useAtom(filterAtom); 21 // Only re-renders when filter changes 22} 23 24function SearchBar() { 25 const [search, setSearch] = useAtom(searchAtom); 26 // Only re-renders when search changes 27}

When to use: Need fine-grained updates without complex selectors.

Pattern 7: URL State#

State that should be shareable:

1function ProductSearch() { 2 const [searchParams, setSearchParams] = useSearchParams(); 3 4 const category = searchParams.get('category') || 'all'; 5 const sort = searchParams.get('sort') || 'popular'; 6 const page = parseInt(searchParams.get('page') || '1'); 7 8 const updateFilters = (updates: Partial<Filters>) => { 9 const newParams = new URLSearchParams(searchParams); 10 Object.entries(updates).forEach(([key, value]) => { 11 newParams.set(key, String(value)); 12 }); 13 setSearchParams(newParams); 14 }; 15 16 return ( 17 <div> 18 <FilterBar 19 category={category} 20 sort={sort} 21 onFilterChange={updateFilters} 22 /> 23 <ProductGrid category={category} sort={sort} page={page} /> 24 </div> 25 ); 26}

When to use: Filters, pagination, view modes—anything users might want to bookmark or share.

Choosing the Right Pattern#

AI can help evaluate your specific situation:

Help me choose a state management approach: Application: E-commerce site State needs: - Shopping cart (persist across pages) - User authentication - Product filters (shareable via URL) - Product data (from API) - UI preferences (theme, currency) - Form state (checkout flow) Team: 3 developers, familiar with React hooks Existing stack: React 18, TypeScript Recommend state management strategy.

Anti-Patterns to Avoid#

Global State for Local Concerns#

1// ❌ Don't put form state in global store 2const useFormStore = create((set) => ({ 3 email: '', 4 password: '', 5 setEmail: (email) => set({ email }), 6 setPassword: (password) => set({ password }), 7})); 8 9// ✅ Keep form state local 10function LoginForm() { 11 const [email, setEmail] = useState(''); 12 const [password, setPassword] = useState(''); 13 // ... 14}

Duplicating Server State#

1// ❌ Don't duplicate API data in store 2const useProductStore = create((set) => ({ 3 products: [], 4 fetchProducts: async () => { 5 const products = await api.getProducts(); 6 set({ products }); 7 }, 8})); 9 10// ✅ Use a server state library 11const { data: products } = useQuery({ 12 queryKey: ['products'], 13 queryFn: api.getProducts, 14});

Over-Engineering Simple Apps#

// ❌ Don't add Redux for a simple app // Store, slices, actions, selectors, middleware... // ✅ Start simple, add complexity when needed const [items, setItems] = useState([]);

Migration Strategies#

From Props Drilling to Context#

Convert this props-drilling pattern to Context: Currently passing 'user' through 5 component levels. Only leaf components actually use 'user'. Provide refactored code with appropriate context structure.

From Context to External Store#

Migrate this Context-based state to Zustand: Current issues: - Too many re-renders - Complex value object - Need for selectors Provide migration path that doesn't break existing consumers.

Conclusion#

State management isn't one-size-fits-all. The best approach depends on:

  • What kind of state you're managing
  • How widely it's used
  • How frequently it updates
  • Whether it needs to survive navigation
  • Your team's familiarity

Start with the simplest approach that works. Add complexity only when you hit real problems. Use AI to evaluate tradeoffs and identify the right pattern for your specific needs.

Share this article

Help spread the word about Bootspring