React Intermediate: Forms, Hooks and State
Context and Reducers
In this lesson you will
- Share data deeply with context
- Write a reducer for predictable state updates
- Know when to reach for an external state library
Introduction
As apps grow, two problems appear. Some data, such as the current user or theme, is needed by many components deep in the tree, and passing it down through every level (prop drilling) is tedious. And some state has so many ways to change that scattered setState calls become hard to follow. Context and reducers address these.
Context
Context lets a parent make a value available to any descendant, without passing it through every layer.
React (JSX)
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}
function Page() {
return <ThemeToggle />; // no props needed
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}createContext makes the context, the Provider supplies the value, and useContext reads it anywhere below.
Good uses: theme, the signed-in user, language. Context re-renders all consumers when the value changes, so do not put rapidly changing data in one giant context.
Quick quiz
What problem does context mainly solve?
useReducer
When state has several related pieces and many kinds of updates, a reducer centralises the rules. A reducer is a pure function: given the current state and an action, it returns the new state.
React (JSX)
import { useReducer } from "react";
function todosReducer(todos, action) {
switch (action.type) {
case "add":
return [...todos, { id: action.id, text: action.text, done: false }];
case "toggle":
return todos.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
case "remove":
return todos.filter((t) => t.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: "add", id: Date.now(), text: "Learn reducers" })}>
Add
</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
<label>
<input type="checkbox" checked={t.done} onChange={() => dispatch({ type: "toggle", id: t.id })} />
{t.text}
</label>
</li>
))}
</ul>
</div>
);
}Benefits: all the ways state can change are in one place, it is easy to test (it is just a function), and components only say what happened (dispatch), not how state changes.
A reducer must be pure: no random numbers, no network calls and no changing the old state. Create new arrays and objects.
Context and reducer together
Put the state and dispatch in a context, and any component can read the data or dispatch actions, a small built-in state management system:
React (JSX)
const TodosContext = createContext(null);
const DispatchContext = createContext(null);
function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext.Provider value={todos}>
<DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
</TodosContext.Provider>
);
}Splitting data and dispatch into two contexts means components that only dispatch do not re-render when the data changes.
When to use a library
Built-in tools are enough for many apps. Libraries such as Redux Toolkit, Zustand or Jotai help with large, complex client state, and server state libraries such as TanStack Query (the next lesson's topic) handle data from APIs. Choose a library when you feel a real pain, not by default.
Try it yourself
Add an "edit text" action to the todo reducer, and an "clear completed" action that removes all done items. Then move the state into a context provider.
Summary
- Context shares values with deeply nested components without prop drilling.
- A reducer is a pure function from state and action to new state, giving predictable updates.
- Combine context and
useReducerfor app-wide state, and add libraries only when needed.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading