State Management
|
This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
As an app grows, the question shifts from "how do I hold state" to "where should this state live and how does it get where it is needed". React’s built-in answers are lifting state up and Context; larger apps often add a library.
Lifting state up
When two components need the same state, move it to their closest common parent and pass it down as props plus a setter. That parent is the single source of truth.
function FilterableList({ items }) {
const [query, setQuery] = useState(''); // lifted here
return (
<>
<SearchBox query={query} onChange={setQuery} />
<Results items={items.filter((i) => i.name.includes(query))} />
</>
);
}
An input is controlled when its value comes from state (value={query} + onChange), and
uncontrolled when the DOM holds the value and you read it with a ref or FormData on submit. Prefer
controlled inputs when you need to validate or react to every keystroke. See
Sharing State Between Components.
Structuring state
-
Group values that change together; keep unrelated values separate.
-
Avoid redundant state — if you can compute it from props or other state during render, do not store it.
-
Avoid duplicated state — store an id, not a copy of the whole selected object.
-
React preserves state by a component’s position in the tree. Rendering a different component type, or the same type at a different position, resets its state. Force a reset by changing the
key:<ProfileForm key={userId} user={user} /> {/* new userId -> fresh form state */}
References: Choosing the State Structure, Preserving and Resetting State.
Context
Context passes a value to every component below a provider without threading props through every level (prop drilling).
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={theme}> {/* React 19: <Context> is the provider */}
<Toolbar />
</ThemeContext>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); // reads nearest provider above
return <button className={theme}>OK</button>;
}
value = 'dark'"] --> A["<Layout>"] A --> B["<Sidebar>"] B --> C["<ThemedButton>
useContext(ThemeContext) returns 'dark'"] P -.->|"no props passed through
Layout or Sidebar"| C
Context is for global-ish data: theme, current user, locale, a router. Do not reach for it just to avoid
passing a prop one or two levels — composition (passing JSX as children) is often enough. Every consumer
re-renders when the context value changes, so memoize the value or split contexts if that becomes a cost.
References: Passing Data Deeply with Context,
createContext,
useContext.
Reducer + context
For app-wide state with non-trivial updates, combine a reducer with context:
one context provides the state, another provides dispatch, and any component can read or update without
prop drilling.
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext>
</TasksContext>
);
}
Third-party libraries
-
Redux Toolkit — the standard modern Redux: a single store, slices, immutable updates via Immer, dev-tools time-travel. Good for large apps with complex shared state and a need for strict traceability.
-
Zustand — a tiny hook-based store, minimal boilerplate.
-
Jotai — atomic state; fine-grained subscriptions.
-
MobX — transparent reactive state via observables.
For server state (data owned by a backend), a dedicated cache — TanStack Query, SWR, or RTK Query — is usually better than any of the above; see Data Fetching. See Managing State for the built-in story.