Context with useReducer in React JS
Context with useReducer is a React technique used to manage complex state and share it across multiple components without passing props through every intermediate component. The useReducer Hook manages state updates through a reducer function, while the Context API makes the state and dispatch function available to descendant components.
Course: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo
1. What Is Context with useReducer?
Context with useReducer combines two React features:
- Context API: Shares data with components at different levels of the component tree without manually passing props through every level.
- useReducer: Manages state by sending actions to a reducer function that calculates the next state.
When used together, a parent component manages the state, and deeply nested components can access the state and dispatch actions through context.
Example Scenario
Imagine a task management application containing an Add Task component, a Task List component, and individual Task components. These components may need to add, update, or delete tasks. Context with useReducer allows them to share and update the same task state without prop drilling.
2. What Is useReducer?
useReducer is a React Hook that manages state using a reducer function. It is particularly useful when state updates involve multiple actions or related pieces of state.
Syntax
const [state, dispatch] = useReducer(reducer, initialState);
| Element |
Description |
state |
The current state value. |
dispatch |
A function used to send an action to the reducer. |
reducer |
A function that calculates and returns the next state. |
initialState |
The initial value of the state. |
Example of a Reducer
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return 0;
default:
throw new Error("Unknown action: " + action.type);
}
}
The reducer receives the current state and an action, then returns the new state. It should be a pure function, meaning it should not directly modify the existing state or perform unrelated side effects.
3. What Is the Context API?
The Context API lets components access shared values without receiving them as props from every parent component.
Important Context API features include:
createContext() creates a context object.
- A context provider supplies a value to descendant components.
useContext() reads the value from the nearest matching provider above the component.
Basic Context Example
import { createContext } from "react";
export const AppContext = createContext(null);
The context is created with null as its default value. The provider supplies the actual value to components inside its subtree.
4. Why Combine Context with useReducer?
Using only useReducer manages state, but sharing that state with deeply nested components may require passing props through several levels. Context removes this repetitive prop passing.
Combining them provides the following benefits:
- Centralizes state update logic in a reducer.
- Reduces prop drilling.
- Allows multiple components to access shared state.
- Allows components to dispatch actions without receiving callback props through every intermediate component.
- Makes complex state transitions easier to organize.
- Separates state management from the presentation of individual components.
5. How to Implement Context with useReducer
Follow these steps to create a simple task management application.
Step 1: Create the Context and Reducer
Create a file named TasksContext.jsx.
import { createContext, useContext, useReducer } from "react";
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
const initialTasks = [
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Practice useReducer", completed: false }
];
function tasksReducer(state, action) {
switch (action.type) {
case "added":
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case "toggled":
return state.map(task =>
task.id === action.id
? { ...task, completed: !task.completed }
: task
);
case "deleted":
return state.filter(task => task.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
Explanation:
TasksContext stores the current task list for consumers.
TasksDispatchContext provides the dispatch function.
initialTasks contains the initial task data.
tasksReducer() handles adding, toggling, and deleting tasks.
- The spread operator and methods such as
map() and filter() create new arrays and objects instead of mutating existing state.
Step 2: Create the Provider Component
Add the following code to the same TasksContext.jsx file.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
export function useTasks() {
const context = useContext(TasksContext);
if (context === null) {
throw new Error("useTasks must be used within TasksProvider");
}
return context;
}
export function useTasksDispatch() {
const context = useContext(TasksDispatchContext);
if (context === null) {
throw new Error("useTasksDispatch must be used within TasksProvider");
}
return context;
}
Explanation:
TasksProvider manages the task state using useReducer.
- The first context provides the current task list.
- The second context provides the dispatch function.
children represents the components wrapped by the provider.
useTasks() and useTasksDispatch() are custom Hooks that simplify access to the two contexts and provide a helpful error if used outside the provider.
Version note: The example above uses React 19 context provider syntax. In React 18 and earlier, replace with and do the same for TasksDispatchContext.
Step 3: Wrap the Application with the Provider
In App.jsx, wrap the components that need access to the shared task state.
import { TasksProvider } from "./TasksContext";
import AddTask from "./AddTask";
import TaskList from "./TaskList";
function App() {
return (
<TasksProvider>
<h1>Task Management App</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
export default App;
Any descendant of TasksProvider can now use the task context and dispatch actions.
Step 4: Create the Add Task Component
Create AddTask.jsx.
import { useState } from "react";
import { useTasksDispatch } from "./TasksContext";
let nextId = 3;
function AddTask() {
const [text, setText] = useState("");
const dispatch = useTasksDispatch();
function handleSubmit(event) {
event.preventDefault();
const trimmedText = text.trim();
if (!trimmedText) return;
dispatch({
type: "added",
id: nextId++,
text: trimmedText
});
setText("");
}
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={event => setText(event.target.value)}
placeholder="Enter a task"
/>
<button type="submit">Add Task</button>
</form>
);
}
export default AddTask;
Explanation: The component reads the dispatch function directly from context and sends an added action. It does not need to receive dispatch through props. The simple nextId variable is suitable for this demonstration; a production application should use a reliable ID strategy appropriate to its data source.
Step 5: Create the Task List Component
Create TaskList.jsx.
import { useTasks } from "./TasksContext";
import TaskItem from "./TaskItem";
function TaskList() {
const tasks = useTasks();
if (tasks.length === 0) {
return <p>No tasks available.</p>;
}
return (
<ul>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
</ul>
);
}
export default TaskList;
Explanation: The Task List component reads the shared state using useTasks() and renders each task. The key helps React identify list items when the list changes.
Step 6: Create the Task Item Component
Create TaskItem.jsx.
import { useTasksDispatch } from "./TasksContext";
function TaskItem({ task }) {
const dispatch = useTasksDispatch();
return (
<li>
<span
style={{
textDecoration: task.completed ? "line-through" : "none"
}}
>
{task.text}
</span>
<button
onClick={() =>
dispatch({ type: "toggled", id: task.id })
}
>
{task.completed ? "Undo" : "Complete"}
</button>
<button
onClick={() =>
dispatch({ type: "deleted", id: task.id })
}
>
Delete
</button>
</li>
);
}
export default TaskItem;
Explanation: Each task item receives only its own task data as a prop. It reads the dispatch function from context to complete or delete the task. This keeps the state update logic centralized in the reducer.
6. How Context with useReducer Works
User clicks a button
|
v
Component calls dispatch(action)
|
v
Reducer receives current state and action
|
v
Reducer returns the next state
|
v
Provider supplies the updated state
|
v
Context consumers render with updated data
For example, when the user clicks Delete, the component dispatches an action containing the task ID. The reducer filters out that task, returns a new array, and React updates the components consuming the task context.
7. Understanding Actions and Reducers
An action describes what happened. A reducer decides how the state should change in response.
| Action Type |
Additional Data |
Purpose |
added |
id, text |
Adds a new task. |
toggled |
id |
Changes a task's completion status. |
deleted |
id |
Removes a task. |
Dispatch Syntax
dispatch({
type: "added",
id: 3,
text: "Build a React project"
});
Calling dispatch() asks React to process the action with the reducer. It does not return the new state directly; the updated state becomes available on the next render.
8. Context with useReducer vs useState
| Feature |
useState |
useReducer with Context |
| State updates |
Usually handled through a setter function. |
Handled by a reducer based on dispatched actions. |
| Shared state |
Can be shared by passing props or using Context. |
Can be shared through Context without prop drilling. |
| Complexity |
Good for simple, local state. |
Useful for complex state transitions shared across components. |
| Organization |
Update logic may be spread across handlers. |
Related update logic can be centralized in the reducer. |
| Setup |
Generally simpler for small components. |
Requires a reducer and context provider setup. |
9. Real-World Use Cases
- Task management: Add, edit, complete, and delete tasks across multiple components.
- Shopping cart: Share cart items and dispatch actions to add or remove products.
- Authentication UI: Share login-related state and provide actions to update it. Authentication security itself must be handled appropriately beyond client-side context.
- Multi-step forms: Manage form data, current step, validation status, and reset actions.
- Dashboard applications: Share filters, selected items, and related UI state across different dashboard components.
- Application preferences: Share settings such as selected display mode or interface preferences.
10. Best Practices
- Keep reducer functions pure and predictable.
- Never directly mutate state arrays or objects; return new values instead.
- Use clear action names, such as
added, updated, and deleted.
- Keep context providers near the part of the application that needs their values when practical.
- Separate state and dispatch into different contexts when that makes the design clearer.
- Create custom Hooks to make context access consistent and easier to maintain.
- Handle unknown actions explicitly during development.
- Use local state for values that are only needed by one component.
- Avoid putting all application data into a single context without a clear reason.
- Remember that consumers of a context update when its provided value changes; consider performance only when measurements indicate a problem.
11. Common Mistakes
| Mistake |
Solution |
| Using a context consumer outside its provider. |
Wrap the component tree with the appropriate provider. |
| Forgetting to return the next state from the reducer. |
Ensure each supported action returns a valid state value. |
| Mutating the existing state directly. |
Use immutable updates with spread syntax, map(), or filter(). |
| Using the wrong action type. |
Check that the dispatched action matches a reducer case. |
| Calling dispatch unconditionally during rendering. |
Dispatch in event handlers or appropriate effects to avoid repeated renders. |
| Using Context for every small state value. |
Choose the simplest suitable solution; local state and props are often enough. |
| Assuming useReducer automatically shares state. |
Place the reducer state and dispatch function in context if descendant components need access. |
12. Interview Questions and Answers
Q1. What is Context with useReducer in React?
It is a pattern that combines reducer-based state management with Context so that multiple descendant components can access and update shared state without prop drilling.
Q2. What is the role of useReducer?
useReducer manages state through a reducer function and a dispatch function. Actions describe events, and the reducer returns the next state.
Q3. Why is Context used with useReducer?
Context makes the state and dispatch function available to descendant components without manually forwarding them through intermediate components.
Q4. What is dispatch?
dispatch is a function returned by useReducer. It sends an action to the reducer to request a state update.
Q5. What is a reducer function?
A reducer is a function that receives the current state and an action and returns the next state.
Q6. Can Context with useReducer replace Redux in every application?
No. Context with useReducer is sufficient for many shared-state scenarios, but larger applications may benefit from dedicated state-management tools and their additional ecosystem, debugging, or performance features.
Q7. Why should state not be mutated directly?
React relies on state identity and predictable updates. Returning new arrays or objects helps React detect changes and keeps reducer logic reliable.
Q8. Can multiple contexts be used with one reducer?
Yes. For example, one context can provide the current state and another can provide the dispatch function.
13. Practical Exercises
- Create a counter application using Context and useReducer with increment, decrement, and reset actions.
- Build a task manager with add, complete, and delete features.
- Create a shopping cart context with actions to add and remove products.
- Build a multi-step form using reducer actions to update fields and move between steps.
- Refactor a component tree that passes state through several intermediate components and replace that prop drilling with Context.
14. Quick Revision Checklist
- Understand the purpose of Context API.
- Understand the syntax and return values of useReducer.
- Know the roles of state, action, reducer, and dispatch.
- Know how to create separate state and dispatch contexts.
- Know how to create a provider component.
- Understand how descendant components read state and dispatch actions.
- Practice immutable state updates.
- Know when to use useState versus useReducer with Context.
Conclusion
Context with useReducer is a useful React pattern for managing complex shared state. The reducer centralizes update logic, while Context makes state and dispatch available to descendant components. This approach can reduce prop drilling and improve code organization when several components need to read or update the same data.
Official React Documentation: Scaling Up with Reducer and Context
React JS Training: JustAcademy React JS Training Course
Course Demo: Register for React JS Course Demo