Bootcamp
Search…
8.3.3: useContext + useReducer

Introduction

In complex app scenarios (our projects are likely not complex enough, but happy to be shown otherwise) with many levels of component nesting and state that is shared across multiple parts of the component tree, it can make sense to use both useContext and useReducer to manage global state. To use both together, we store global state in a reducer passed to useReducer, and pass useReducer's resulting dispatch function to child components through context to manipulate state.

Example

The following is a Todo List example using useContext and useReducer from Richard Kotze's blog. Rocket has added inline comments for explanation. Here is a live example of the below code.
App.js
import './styles.css';
​
// We can use useState alongside useReducer
import React, { useState, useReducer, useContext } from 'react';
​
// State-management code is in ./todo
import {
toDoReducer,
initialState,
addAction,
markAction,
deleteAction,
} from './todo';
​
// Create empty global state
const TodosContext = React.createContext(null);
​
export default function App() {
// todoList is the state variable, and dispatch is the function we can
// use to manipulate todoList. The dispatch function wraps the toDoReducer
// reducer function passed to useReducer.
const [todoList, dispatch] = useReducer(toDoReducer, initialState);
return (
// Set the value of global state the dispatch function to make
// the dispatch function available in all components without
// passing props.
<TodosContext.Provider value={dispatch}>
<div className="App">
<TaskForm />
<h3>Tasks</h3>
<TaskList tasks={todoList} />
</div>
</TodosContext.Provider>
);
}
​
function TaskForm() {
// The TaskForm component has local state managed by useState
const [task, setTask] = useState('');
// Retrieve the dispatch function from global state with useContext
const dispatch = useContext(TodosContext);
​
function handleSubmit(e) {
e.preventDefault();
// On form submit, run dispatch function with the add action object
// generated by the addAction function to add a task to the todoList
// state stored in the App component. The action object tells dispatch
// which action to run to manipulate the state. We define the behaviours
// of each action in the reducer function toDoReducer.
dispatch(addAction(task));
// Reset the local task form state
setTask('');
}
​
return (
<form onSubmit={handleSubmit}>
<p>
<input
type="text"
name="task"
className="add-task"
onChange={(e) => {
setTask(e.target.value);
}}
autoComplete="off"
value={task}
placeholder="Add a task"
/>
<button className="baseButton add" type="submit">
Add
</button>
</p>
</form>
);
}
​
function TaskList({ tasks }) {
// Retrieve the dispatch function from global state with useContext
const dispatch = useContext(TodosContext);
​
// Dispatch the mark action that affects the todoList state in the
// App component.
const dispatchMark = (id, value) => {
dispatch(markAction(id, value));
};
​
return (
<ol>
{tasks.map((task, i) => {
return (
<li key={i}>
<span className={task.done && `cross-out`}>{task.name}</span>{' '}
{!task.done && (
<button
id={i}
className="baseButton done"
onClick={() => dispatchMark(i, true)}
>
Done
</button>
)}
{task.done && (
<button
id={i}
className="baseButton undo"
onClick={() => dispatchMark(i, false)}
>
Undo
</button>
)}
// The delete button dispatches the delete action.
<button
id={i}
className="baseButton delete"
onClick={() => dispatch(deleteAction(i))}
>
Delete
</button>
</li>
);
})}
</ol>
);
}

todo.js

Manage everything about the todo items list data in this file.
todo.js
export const initialState = [
{
name: 'Add a task item',
done: false,
},
];
​
const ADD = 'ADD';
const MARK = 'MARK';
const DELETE = 'DELETE';
​
export function toDoReducer(state, action) {
switch (action.type) {
case ADD:
return [...state, action.payload.task];
case MARK:
return state.map((task, i) => {
if (i === action.payload.taskId)
return { ...task, done: action.payload.done };
​
return task;
});
case DELETE:
return state.filter((_task, i) => action.payload.taskId !== i);
default:
return state;
}
}
​
// The following action-generating functions are commonly referred to
// as "action creators". They accept any input relevant to the action,
// and return an object that represents that action, which is typically
// passed to the dispatch function. Actions always contain a type attribute
// used to identify the action and tell the reducer what logic to run.
export function addAction(taskText) {
return {
type: ADD,
payload: {
task: {
name: taskText,
done: false,
},
},
};
}
​
export function markAction(taskId, done) {
return {
type: MARK,
payload: {
taskId,
done,
},
};
}
​
export function deleteAction(taskId) {
return {
type: DELETE,
payload: {
taskId,
},
};
}
Copy link