Suggest an editImprove this articleRefine the answer for “State management approaches in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**State management in React** is the system for storing and sharing data across components, from `useState` to global stores and server caches. ```tsx const [count, setCount] = useState(0); // local const cartCount = useCartStore(s => s.items.length); // global, selector ``` **Key rule:** start with `useState`, use Zustand for shared UI state, TanStack Query for API data.Shown above the full answer for quick recall.Answer (EN)Image**State management in React** is the system for storing, updating, and sharing data across components, from a single `useState` call to a global store or a server cache. ## Theory ### TL;DR - `useState` is a personal shelf: fast, local, zero setup - Context is a shared fridge: app-wide, but every consumer re-renders on change - Zustand covers 90% of global state needs with almost no boilerplate (+3KB) - TanStack Query handles server state specifically: caching, background refetch, mutations - Decision rule: fewer than 3 components sharing data? `useState`. Complex async? TanStack Query. Global UI state? Zustand. ### Quick example ```tsx // Local state: one component keeps its own counter function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } // Shared state: theme available anywhere without prop drilling const ThemeContext = createContext<"light" | "dark">("light"); function App() { const [theme, setTheme] = useState<"light" | "dark">("light"); return ( <ThemeContext.Provider value={theme}> <Child /> {/* reads theme via useContext, no prop needed */} </ThemeContext.Provider> ); } ``` `useState` re-renders only its own component. Context re-renders every consumer. That gap matters at scale. ### The core split React state falls into three categories by scope. **Local state** (`useState`, `useReducer`) stays in one component and triggers re-renders only there. **Shared state** (Context, Zustand, Redux) broadcasts changes across the tree for data like user auth or a shopping cart. **Server state** (TanStack Query, SWR) is a separate category: it handles async fetches, caching, and background sync, things that have nothing to do with UI state. Mixing these categories causes the most confusion I see in production codebases. A team puts API responses into Redux, then wonders why cache invalidation is so painful. ### When to use each approach - **Single component** (toggle, input, modal open) → `useState`, no discussion needed - **Complex local logic** (multi-step form, undo history) → `useReducer` with a typed action union - **2-5 components sharing infrequently changing data** (theme, locale, auth token) → Context API - **App-wide state that changes often** (cart, UI preferences, notifications) → Zustand for a minimal API or Redux Toolkit for larger teams with strict conventions - **Data from an API** (user list, search results, paginated posts) → TanStack Query or SWR - **Complex forms with validation** → React Hook Form, which is uncontrolled and avoids re-renders on every keystroke - **Filters, pagination, sort order** → URL params via `URLSearchParams`, which survive page refresh and are shareable ### Comparison table | Approach | Scope | Boilerplate | Async handling | Bundle size | Best for | |---|---|---|---|---|---| | `useState` | Local | None | Manual | 0 | UI toggles, inputs | | `useReducer` | Local/shared | Low | Manual | 0 | Complex local logic | | Context API | App-wide | Low | Manual | 0 | Theme, auth, locale | | Zustand | Global | Minimal | Via plugins | +3KB | Most apps | | Redux Toolkit | Global | Medium | RTK Query | +12KB | Large teams, legacy | | TanStack Query | Server | Low | Built-in cache | +14KB | API-heavy apps | | React Hook Form | Forms | Low | N/A | +9KB | Validation | ### How it works internally React queues state updates in fiber nodes. Each `useState` call hooks into the dispatcher, attaching an update queue per hook index. In React 18, updates inside event handlers are automatically batched, so multiple `setState` calls produce one re-render instead of several. Context providers create a tree walker. When the context value changes, React walks the subtree and marks consumer components for re-render. There is no built-in selector mechanism: if the value object changes reference, all consumers re-render. That is why placing a large object in Context without memoization is a performance problem. TanStack Query uses a global `Map` keyed by query keys. Fetches go through `AbortController`, so navigating away from a page cancels in-flight requests. The Visibility API triggers background refetch when the user tabs back. All of this runs outside React's state model entirely. ### Common mistakes **Lifting all state to the top and passing it down.** ```tsx // Wrong: every child re-renders on any cart change function App() { const [cart, setCart] = useState([]); return <Header cart={cart} />; // Header re-renders even if it only shows item count } // Fix: subscribe to only what you need via a selector const cartCount = useCartStore(state => state.items.length); // Re-renders only when count changes, not on any cart mutation ``` **Mutating state directly.** ```tsx // Wrong: React won't detect this, no re-render happens const addItem = () => { cart.push(newItem); setCart(cart); // same reference, React bails out }; // Fix: create a new array reference setCart([...cart, newItem]); ``` **Missing dependency in TanStack Query keys.** ```tsx // Wrong: cache never updates when userId changes useQuery({ queryKey: ["posts"], queryFn: () => fetchPosts(userId) }); // Fix: userId belongs in the key useQuery({ queryKey: ["posts", userId], queryFn: () => fetchPosts(userId) }); ``` **Putting everything in one Context.** When a single Context holds too much, any change re-renders the entire subtree. Split into smaller, focused Contexts or move to Zustand. ### Real-world usage - Next.js apps at Vercel use TanStack Query for data fetching with stale-while-revalidate patterns - Shopify admin uses Redux Toolkit with structured slices for complex e-commerce state - Discord web client uses Zustand for lightweight global UI state - Stripe checkout uses React Hook Form for validation without per-keystroke re-renders - Most teams that reach for Redux first end up with Zustand after a refactor ### Follow-up questions **Q:** When does `useState` cause performance issues? **A:** When the same state is shared across a deep tree via props. Every `setState` triggers re-renders in every consumer. The fix is moving to Context with memoization or to Zustand selectors. **Q:** What is the difference between Zustand and Redux Toolkit? **A:** Redux has more structure: actions, reducers, slices, and strict conventions that help large teams stay consistent. Zustand reaches the same result with a fraction of the code. For a team of 2-5 developers, Zustand is almost always the right pick. **Q:** How does TanStack Query prevent request waterfalls? **A:** Parallel queries via suspense boundaries and `prefetchQuery` at the router level, so data loads before components mount. **Q:** What is wrong with `useReducer` plus Context for large apps? **A:** Without selectors, every context value change re-renders the whole tree. There is no built-in way to subscribe to a slice of context. Zustand solves this with `useStore(selector)`. **Q:** In React 18, how do transitions affect state updates? **A:** `useTransition` marks updates as low-priority. A heavy filter computation wrapped in a transition keeps the input responsive while results update in the background. **Q:** Describe optimistic updates in TanStack Query. **A:** In `useMutation`, set `onMutate` to update the cache immediately and save the previous value, then call `onError` to roll back if the server returns an error. ## Examples ### Local state: a controlled input ```tsx function SearchInput() { const [query, setQuery] = useState(""); return ( <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." /> ); } // Each keystroke updates query, only this component re-renders ``` Controlled inputs are the most common use of `useState`. The component owns the value, React owns the render cycle. ### Global state: a Zustand cart store ```tsx import { create } from "zustand"; interface CartItem { id: number; qty: number; price: number; } interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: number) => void; total: () => number; } export const useCartStore = create<CartStore>((set, get) => ({ items: [], addItem: (item) => set(state => ({ items: [...state.items, item] })), removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id), })), total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0), })); // Only re-renders when item count changes, not on price updates function CartBadge() { const count = useCartStore(state => state.items.length); return <span className="badge">{count}</span>; } ``` The selector `state => state.items.length` is what makes Zustand performant here. `CartBadge` re-renders only when the count changes, not when individual prices or quantities update. ### Server state: TanStack Query with a mutation ```tsx import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; function UserList() { const queryClient = useQueryClient(); const { data: users, isLoading } = useQuery({ queryKey: ["users"], queryFn: () => fetch("/api/users").then(r => r.json()), }); const deleteUser = useMutation({ mutationFn: (id: number) => fetch(`/api/users/${id}`, { method: "DELETE" }), onSuccess: () => { // Marks cache as stale, triggers automatic background refetch queryClient.invalidateQueries({ queryKey: ["users"] }); }, }); if (isLoading) return <p>Loading...</p>; return ( <ul> {users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => deleteUser.mutate(user.id)}>Delete</button> </li> ))} </ul> ); } ``` `invalidateQueries` after a mutation marks the cache as stale and triggers a background refetch. No manual `useEffect`, no state juggling.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.