In the previous blog posts, I discussed state management and how it works with useState. In this post, I'll cover useReducer and how to manage state in React.
- What is a Reducer?
- Why do we use it?
- How Reducer Works
What is a Reducer?
A reducer is a function used with the useReducer
hook, which needs to be imported at the top of your file.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
Why do we use it?
As an app grows larger and more state variables are required across multiple components, it becomes harder to manage, test, and debug them. While useState
and useReducer
are similar in functionality, useReducer
can make our code easier to handle. By using a reducer, we can create a separate file that contains only the reducer function instead of writing it in each component. This reduces the amount of code within individual components, which is one of the reasons why it is called a "reducer."
How Reducer Works
With useState
, we update the state by using an updater function (e.g., setUser
in the example below):
const [user, setUser] = useState('');
With useReducer
, we dispatch an action that describes what the user did, along with the current state. The reducer function then processes the action and returns the updated state.
Let's take a look at the example shared in the React Documentation.
This app contains a TaskApp
component that handles adding, editing, and deleting tasks for a trip to Prague.
This component has three event handlers (for adding, editing, and deleting) to update the state (setTasks
).
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
It's better to write these functions in a separate file, so here's the tasksReducer.js
file:
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);
}
}
}
A reducer function needs two parameters: one is the current state
(tasks
) and the other is the action
. The action is an object that contains a type
(which describes the action the user performed) and additional data, such as id
or task
, that the reducer function needs. The reducer function returns the updated (or next
) state.
In each component, we declare useReducer
at the top of the file:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
We then use the dispatch function in an event handler.
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.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>Prague itinerary</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{id: 0, text: 'Visit Kafka Museum', done: true},
{id: 1, text: 'Watch a puppet show', done: false},
{id: 2, text: 'Lennon Wall pic', done: false},
];
Two important rules when we use useReducer:
- Reducers must be pure: This means that for the same inputs, the reducer must always return the same outputs, with no side effects.
- Each action describes a single user interaction: This means one action should handle one specific task or change, even if that task leads to multiple changes in the data.
Even though useReducer
may seem more complex at first glance due to its additional code and structure compared to useState, it ultimately makes the code more readable and manageable in larger applications.
Top comments (0)