State management in large React apps requires choosing the right tool for each type of state: local UI state, shared app state, server state, and URL state.
1. Zustand — lightweight and recommended for most cases:
1import { create } from "zustand";2import { persist } from "zustand/middleware";34interface TodoStore {5 todos: Todo[];6 filter: "all" | "active" | "completed";7 addTodo: (text: string) => void;8 toggleTodo: (id: string) => void;9 setFilter: (filter: TodoStore["filter"]) => void;10}1112const useTodoStore = create<TodoStore>()(13 persist(14 (set) => ({15 todos: [],16 filter: "all",17 addTodo: (text) => set((state) => ({18 todos: [...state.todos, { id: crypto.randomUUID(), text, done: false }],19 })),20 toggleTodo: (id) => set((state) => ({21 todos: state.todos.map(t =>22 t.id === id ? { ...t, done: !t.done } : t23 ),24 })),25 setFilter: (filter) => set({ filter }),26 }),27 { name: "todo-storage" }28 )29);3031// Usage — components only re-render when selected state changes32function TodoList() {33 const todos = useTodoStore(state => state.todos);34 const filter = useTodoStore(state => state.filter);35 const toggleTodo = useTodoStore(state => state.toggleTodo);3637 const filtered = todos.filter(t => {38 if (filter === "active") return !t.done;39 if (filter === "completed") return t.done;40 return true;41 });4243 return (44 <ul>45 {filtered.map(t => (46 <li key={t.id} onClick={() => toggleTodo(t.id)}>47 {t.text}48 </li>49 ))}50 </ul>51 );52}
2. Redux Toolkit — for complex state with many actors:
1import { createSlice, configureStore } from "@reduxjs/toolkit";2import { Provider, useSelector, useDispatch } from "react-redux";34const todosSlice = createSlice({5 name: "todos",6 initialState: [] as Todo[],7 reducers: {8 addTodo: (state, action) => {9 state.push({ id: crypto.randomUUID(), text: action.payload, done: false });10 },11 toggleTodo: (state, action) => {12 const todo = state.find(t => t.id === action.payload);13 if (todo) todo.done = !todo.done;14 },15 },16});1718const store = configureStore({ reducer: { todos: todosSlice.reducer } });1920// Usage21function TodoApp() {22 const todos = useSelector(state => state.todos);23 const dispatch = useDispatch();2425 return (26 <div>27 {todos.map(t => (28 <div key={t.id} onClick={() => dispatch(todosSlice.actions.toggleTodo(t.id))}>29 {t.text}30 </div>31 ))}32 </div>33 );34}
3. TanStack Query — for server state:
1import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";23function Todos() {4 const queryClient = useQueryClient();56 const { data: todos, isLoading } = useQuery({7 queryKey: ["todos"],8 queryFn: () => fetch("/api/todos").then(r => r.json()),9 });1011 const addTodo = useMutation({12 mutationFn: (text: string) =>13 fetch("/api/todos", { method: "POST", body: JSON.stringify({ text }) }),14 onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),15 });1617 if (isLoading) return <p>Loading...</p>;18 return (19 <ul>20 {todos.map(t => <li key={t.id}>{t.text}</li>)}21 <button onClick={() => addTodo.mutate("New todo")}>Add</button>22 </ul>23 );24}
Decision guide:
useState / useReduceruseSearchParams