State Management Overview
“State management” sounds like a single tool you bolt onto an app, but in React it is really a spectrum of techniques—from a single useState call to a full server-cache library. The skill is not picking the library; it is matching each piece of state to the lightest mechanism that can hold it. This page maps the whole landscape so you know which tool to reach for and when, and links to the deep-dive pages for each one.
The state ladder
Most state questions are answered by climbing a ladder from local to global and stopping at the first rung that works. Reaching for a global store before you need one is the most common source of accidental complexity in React apps.
| Rung | Mechanism | Use when |
|---|---|---|
| 1. Local | useState / useReducer | One component owns the value |
| 2. Lifted | State in a common parent, passed via props | A few nearby components share it |
| 3. Context | useContext + a provider | Many components across a subtree read rarely-changing data |
| 4. Global store | Redux Toolkit, Zustand, Jotai, MobX | App-wide client state with frequent, complex updates |
| 5. Server state | TanStack Query, RTK Query, SWR | The data actually lives on a server |
Climb only as high as you must. If lifting one prop solves the problem, you do not need Context; if Context solves it, you do not need Redux.
Local state
Local state belongs to one component and dies with it. It covers the vast majority of UI state: input values, toggles, hover flags, and the like. Use useState for independent values and useReducer when several values change together by well-defined actions.
import { useState } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
return (
<input
value={query}
onFocus={() => setOpen(true)}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
When the update logic grows branchy, a reducer keeps it testable—see reducer patterns.
Lifted and shared state
When two siblings need the same value, move it to their nearest common parent and pass it down. This “lifting state up” pattern keeps a single source of truth without any library at all. It is the right answer far more often than developers expect.
import { useState } from "react";
function Filters() {
const [category, setCategory] = useState("all");
return (
<>
<CategoryPicker value={category} onChange={setCategory} />
<ProductList category={category} />
</>
);
}
The trade-off is prop drilling: passing a value through layers that do not use it. A little drilling is fine; deep drilling is the signal to climb to Context.
Context for cross-cutting data
Context broadcasts a value to an entire subtree without threading props through every level. It is ideal for low-frequency, app-wide concerns: the current theme, the authenticated user, the active locale.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={`bar bar-${theme}`}>Current theme: {theme}</div>;
}
Context is not a state manager—it is a transport. Every consumer re-renders when the value changes, so for fast-changing data it can hurt performance. Read Context API and Context performance before using it as a store.
Global stores
When client state is large, updated frequently, or read from many unrelated places, a dedicated store earns its keep. The major options differ mostly in philosophy.
| Library | Model | Boilerplate | Notes |
|---|---|---|---|
| Redux Toolkit | Single store, reducers, actions | Low (vs old Redux) | Predictable, great devtools, huge ecosystem |
| Zustand | Hook-based store, no provider | Minimal | Tiny, ergonomic, easy to adopt incrementally |
| Jotai | Atomic, bottom-up | Minimal | Fine-grained reactivity, derived atoms |
| MobX | Observable, reactive | Low | Mutate-and-go, less explicit data flow |
A minimal Zustand store shows how little ceremony a modern store needs:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
function CartButton() {
const count = useCartStore((s) => s.items.length);
return <button>Cart ({count})</button>;
}
Explore each in depth: Redux Toolkit, Zustand, and Jotai.
Server state is different
The biggest mindset shift is recognizing that data fetched from an API is not really your state—it is a cache of state that lives elsewhere. It can go stale, needs refetching, and must handle loading and error conditions. Stuffing it into useState or Redux forces you to reinvent caching, deduping, and invalidation by hand.
Server-state libraries solve this directly. They cache by key, dedupe requests, refetch in the background, and expose loading/error flags for free.
import { useQuery } from "@tanstack/react-query";
function Profile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ["user", userId],
queryFn: () =>
fetch(`/api/users/${userId}`).then((r) => r.json()),
});
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Something went wrong.</p>;
return <h1>{data.name}</h1>;
}
Output:
Loading…
(then, once the request resolves)
Ada Lovelace
Reach for RTK Query if you already use Redux, or TanStack Query / SWR otherwise. Once server state is handled, most apps need surprisingly little global client state.
Picking a mix
Real apps combine several rungs at once: local state for form fields, Context for theme and auth, a small global store for cross-page UI, and a server-cache library for everything from the API. There is no single “correct” stack—only the right tool per category. When in doubt, start small and promote state upward only when sharing genuinely requires it.
Best Practices
- Default to local state; climb the ladder only when sharing forces you to.
- Treat server data as a cache with a dedicated library—do not store it manually.
- Use Context for stable, cross-cutting values, not for rapidly changing data.
- Keep client and server state in separate systems; they have different lifecycles.
- Store the minimum—derive everything you can instead of duplicating it.
- Normalize relational data so each entity has one source of truth.
- Choose a global store for its update model and ergonomics, not its popularity.