Skip to main content

6.3 - Combining Reducers and Context

Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen.


Combining a reducer with context

In this example from the introduction to reducers, the state is managed by a reducer. The reducer function contains all of the state update logic and is declared at the bottom of this file:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);

function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}

return (
<>
<h1>Day off in Kyoto</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}

function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];

But currently, the tasks state and the dispatch function are only available in the top-level TaskApp component. To let other components read the list of tasks or change it, you have to explicitly pass down the current state and the event handlers that change it as props.

For example, TaskApp passes a list of tasks and the event handlers to TaskList:

App.js
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

And TaskList passes the event handlers to Task:

TaskList.js
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

As an alternative to passing them through props, you might want to put both the tasks state and the dispatch function into context. This way, any component below TaskApp in the tree can read the tasks and dispatch actions without the repetitive “prop drilling”.

Here is how you can combine a reducer with context:

  1. Create the context.
  2. Put state and dispatch into context.
  3. Use context anywhere in the tree.

Step 1: Create the context

Remember that the useReducer Hook returns the current tasks and the dispatch function that lets you update them:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

To pass them down the tree, you will create two separate contexts:

  • TasksContext provides the current list of tasks.
  • TasksDispatchContext provides the function that lets components dispatch actions.

Export them from a separate file (here named TasksContext.js) so that you can later import them from other files:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

Here, you’re passing null as the default value to both contexts. The actual values will be provided by the TaskApp component.


Step 2: Put state and dispatch into context

Now you can import both contexts in your TaskApp component. Take the tasks and dispatch returned by useReducer() and provide them to any of TaskApp's children components:

App.js
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
<TasksContext.Provider value={tasks}>

<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

For now, you pass the information both via props and in context. In the next step, you will remove prop passing.


Step 3: Use context anywhere in the tree

Now you don’t need to pass the list of tasks or the event handlers down the tree:

App.js
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);

// No longer need to list out individual event handlers here! (handleAddTask, handleDeleteTask, etc.)

return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask /> // no more prop passing!
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);

function tasksReducer(tasks, action) {
// ...

Now, any component that needs the task list can read it from the TaskContext:

export default function TaskList() {
const tasks = useContext(TasksContext);
// ...

To update the task list, any component can read the dispatch function from context and call it:

export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...

Moving all wiring into a single file

You don't have to do this, but you can move the tasksReducer function in App.js to the relatively empty TasksContext.js file. This helps declutter App.js.

You can also consolidate the Provider code into a new component called TasksProvider and put it into TasksContext.js also.

The final result looks like this:

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}

Recap

  • You can combine reducer with context to let any component read and update state above it.
  • To provide state and the dispatch function to components below:
    1. Create two contexts (for state and for dispatch functions).
    2. Provide both contexts from the component that uses the reducer.
    3. Use either context from components that need to read them.