Choose useState
- Good fit
- A few independent values with straightforward updates.
- Benefit
- Less setup and direct setter calls.
A reducer puts related state transitions in one function. Context makes a value available to descendants. Together, they can give a feature one clear state owner without passing the same state and handlers through every intermediate component.
useReducer is a state Hook. Like useState, it stores state between renders. Instead of calling a different setter for each change, an event handler dispatches an action that describes what happened. React passes the current state and that action to the reducer, then renders with the state the reducer returned.
dispatch({ type: "added", ... }).dispatch schedules the next render. Reading state immediately after dispatch still gives the current render's snapshot, not the state for the next render.
import { useReducer, useState } from "react";
function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [
...tasks,
{ id: action.id, text: action.text, done: false }
];
case "toggled":
return tasks.map(task =>
task.id === action.id
? { ...task, done: !task.done }
: task
);
case "deleted":
return tasks.filter(task => task.id !== action.id);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
let nextId = 2;
const initialTasks = [
{ id: 1, text: "Learn reducers", done: false }
];
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const [text, setText] = useState("");
function handleAdd() {
if (!text.trim()) return;
dispatch({ type: "added", id: nextId++, text });
setText("");
}
return (
<section>
<input
value={text}
onChange={event => setText(event.target.value)}
/>
<button onClick={handleAdd}>Add</button>
<ul>
{tasks.map(task => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => dispatch({
type: "toggled", id: task.id
})}
/>
{task.text}
</label>
<button onClick={() => dispatch({
type: "deleted", id: task.id
})}>
Delete
</button>
</li>
))}
</ul>
</section>
);
}
A reducer is a regular JavaScript function with the shape (state, action) => nextState. It should describe state transitions, while event handlers describe events from the interface.
For the same state and action, return the same result. Do not send requests, start timers, read random values, or modify variables outside the reducer.
Return new objects and arrays instead of mutating the current state. React may call reducers more than once in development Strict Mode to expose accidental impurities.
Types such as "task_added" and "form_submitted" explain what happened. Put the data needed for that transition in the action object.
A single action can update several related fields. This keeps the full transition together and prevents partial updates.
Throwing in the default branch reveals misspelled or unhandled action types during development.
A reducer does not make state global. useReducer still owns state in the component that calls it. Lift that component or add context only when descendants genuinely need the same state.
Context lets a parent provide a value to any descendant below it, even through components that do not use that value. A component calling useContext(SomeContext) reads the value from the closest matching provider above it.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function Toolbar() {
// Toolbar does not need to receive or forward a theme prop.
return <SaveButton />;
}
function SaveButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Save</button>;
}
export default function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme(
theme === "dark" ? "light" : "dark"
)}>
Switch theme
</button>
</ThemeContext.Provider>
);
}
Providers can be nested. A consumer reads the first matching provider found while moving upward through the rendered tree.
The value passed to createContext is used only when no matching provider exists above the consumer. It does not change when provider state changes.
It transports a value. State still belongs to a component using useState or useReducer, and that component passes the value to the provider.
Theme, authenticated user, routing, and feature-level state are common examples. Ordinary props remain clearer when only a few nearby components need the data.
A common feature pattern puts useReducer in a provider component. One context supplies the current state and another supplies dispatch. Descendants can then read state or request updates without receiving both values through every intermediate component.
import {
createContext,
useContext,
useReducer
} from "react";
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
const tasks = useContext(TasksContext);
if (tasks === null) {
throw new Error(
"useTasks must be used inside TasksProvider"
);
}
return tasks;
}
export function useTasksDispatch() {
const dispatch = useContext(TasksDispatchContext);
if (dispatch === null) {
throw new Error(
"useTasksDispatch must be used inside TasksProvider"
);
}
return dispatch;
}
function TaskCount() {
const tasks = useTasks();
return <p>{tasks.length} tasks</p>;
}
function DeleteButton({ taskId }) {
const dispatch = useTasksDispatch();
return (
<button onClick={() => dispatch({
type: "deleted",
id: taskId
})}>
Delete
</button>
);
}
function App() {
return (
<TasksProvider>
<TaskCount />
<TaskList />
</TasksProvider>
);
}
Splitting state and dispatch is useful because components that only dispatch do not also subscribe to the changing state context. Components that read TasksContext render again when its provided value changes. Reach for further optimization only after measuring a real performance problem.
Use useState when updates are simple. Move to useReducer when related transitions become difficult to understand or test.
The component calling the state Hook owns the state, even when context makes it available elsewhere.
Context reduces repeated prop forwarding. A reducer organizes updates. Neither automatically requires the other.
Wrap the smallest meaningful feature subtree so dependencies and update scope remain clear.
It must return the complete next state for the action. It should calculate that state without mutating the current state or causing side effects.
useContext create shared state?
No. Context reads a provided value. A component still owns changing data with a state Hook and supplies that data through a provider.
It gives consumers a clearer dependency. A component that only dispatches actions does not need to subscribe to the changing state context.