Reducer
- Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.
- Reducers are a different way to handle state.
- You can migrate from useState to useReducer in three steps:
- Move from setting state to dispatching actions.
- Write a reducer function.
- Use the reducer from your component.
Without Reducer
TaskApp
let nextId = 3;
const initialTasks = [
{id: 0, text: 'Buy Groceries', done: true},
{id: 1, text: 'Learn React', done: false},
{id: 2, text: 'Do Exercise', done: false},
];
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));
}
return (
<>
<h1>Task List</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
AddTask.js
export default function AddTask({onAddTask}) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button
onClick={() => {
setText('');
onAddTask(text);
}}>
Add
</button>
</>
);
}
TaskList.js
export default function TaskList({tasks, onChangeTask, onDeleteTask}) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={onChangeTask} onDelete={onDeleteTask} />
</li>
))}
</ul>
);
}
Task.js
function Task({task, onChange, onDelete}) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={(e) => {
onChange({
...task,
text: e.target.value,
});
}}
/>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={(e) => {
onChange({
...task,
done: e.target.checked,
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>Delete</button>
</label>
);
}
Move from setting state to dispatching actions.
- Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers.
- The object you pass to dispatch is called an “action”:
- Action is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened.
const addTask = () => {
dispatch({ type: "added", id: nextId++, text: "test" });
};
const updateTask = (task: any) => {
dispatch({ type: "changed", task: task });
};
const deleteTask = (id: any) => {
dispatch({ type: "deleted", id: id });
};
Write a reducer function.
- A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state
- Reducers must be pure.
- Each action describes a single user interaction, even if that leads to multiple changes in the data
function tasksReducer(tasks: any, action: any) {
switch (action.type) {
case "added": {
return [...tasks, { id: action.id, text: action.text, done: false }];
}
case "changed": {
return tasks.map((t: any) => (t.id === action.task.id ? action.task : t));
}
case "deleted": {
return tasks.filter((t: any) => t.id !== action.id);
}
default: {
throw "Invalid action type";
}
}
Use the reducer from your component.
useReducer
takes two arguments a reducer function and an initial stateuseReducer
returns a stateful value and a dispatch function (to “dispatch” user actions to the reducer)
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);