We usually use reducers when we start having a bunch of setState() growing all over the codebase. It is also a way to manage the logic and put them into one place.
Consider the code below.
import { useReducer, useState } from 'react';
const App = () => {
const [tasks, setTasks] = [
{id: 0, text: 'Visit Museum', done: true},
{id: 1, text: 'Watch a puppet show', done: false},
{id: 2, text: 'Lennon Wall pic', done: false},
]
const [inputText, setInputText] = useState('')
const onInputTextChange = (e) => {
setInputText(e.target.value)
}
const handleTaskAdd = () => {
// setting state here
setTasks(
[
...tasks,
{id: tasks.length, text: inputText, done: false}
]
)
setInputText('')
}
const handleTaskDelete = (taskId) => {
// setting state here
setTasks(tasks.filter(task => task.id !== taskId))
}
return (
<div>
<input type="text" value={inputText} onChange={onInputTextChange} />
<button onClick={handleTaskAdd}>add task</button>
<ul>
{tasks.map(task => {
return(
<div key={task.id}>
<p>{task.text}</p>
<button onClick={() => handleTaskDelete(task.id)}>Delete</button>
</div>
)
})}
</ul>
</div>
)
}
As you can see, there are some logic attached to the setState() portions of the code. This looks uncluttered and unmessy now, but when the codebase becomes larger, this is going to be an issue.
The main idea behind reducers is to, well, reduce. We essentially separate out the logic and state management into a part on its own.
The reducer holds and/or accepts 2 things.
-
tasksis the current state -
actionis the object relayed from thedispatchcall.
As you can see in the code below, action carries several things, what type of change it is and the payload itself (or the content of said change).
const taskReducer = (tasks, action) => {
switch (action.type) {
case 'add': {
const arr = [
...tasks,
{id: tasks.length, text: action.text, done: false}
]
return arr
}
case 'delete': {
const arr = tasks.filter(task => task.id !== action.taskId)
return arr
}
default: {
throw Error(`Unknown error: ${action.type}`)
}
}
}
Reducers are a mindset shift. Instead of setting states, we are sending out "actions".
In reducers, we mostly interact with the actions object.
- we check what type of change it is
- we deal with the logic and return the new state (we are utilising the
tasksstate in the logic here)
So, we're done with the main logic. How do we utilise it?
In the file that we would like to use the reducer in, import useReducer.
import { useReducer, useState } from 'react';
In the component that you are planning to use reducers in, do this.
const initialTasks = [
{id: 0, text: 'Visit Museum', done: true},
{id: 1, text: 'Watch a puppet show', done: false},
{id: 2, text: 'Lennon Wall pic', done: false},
]
const [tasks, dispatch] = useReducer(taskReducer, initialTasks)
Now, you have the initial data, initialTasks.
useReducer takes in the reducer itself and the initial state to hold and it returns [tasks, dispatch]. Notice something familiar?
we just turned
const [tasks, setTasks] = [
{id: 0, text: 'Visit Museum', done: true},
{id: 1, text: 'Watch a puppet show', done: false},
{id: 2, text: 'Lennon Wall pic', done: false},
]
into
const [tasks, dispatch] = useReducer(taskReducer, initialTasks)
Same mechanics.
Now, instead of:
- dealing with the logic in the same file or line of code, you outsource it to the reducer
- setting tasks using
setTasks, you calldispatch
This is the updated code.
import { useReducer, useState } from 'react';
const App = () => {
const initialTasks = [
{id: 0, text: 'Visit Museum', done: true},
{id: 1, text: 'Watch a puppet show', done: false},
{id: 2, text: 'Lennon Wall pic', done: false},
]
const [tasks, dispatch] = useReducer(taskReducer, initialTasks)
const [inputText, setInputText] = useState('')
const onInputTextChange = (e) => {
setInputText(e.target.value)
}
const handleTaskAdd = () => {
// copy array and append from there
// setTasks(
// [
// ...tasks,
// {id: tasks.length, text: inputText, done: false}
// ]
// )
dispatch({
type: 'add',
text: inputText
})
setInputText('')
}
const handleTaskDelete = (taskId) => {
// setTasks(tasks.filter(task => task.id !== taskId))
dispatch({
type: 'delete',
taskId: taskId
})
}
return (
<div>
<input type="text" value={inputText} onChange={onInputTextChange} />
<button onClick={handleTaskAdd}>add task</button>
<ul>
{tasks.map(task => {
return(
<div key={task.id}>
<p>{task.text}</p>
<button onClick={() => handleTaskDelete(task.id)}>Delete</button>
</div>
)
})}
</ul>
</div>
)
}
Notice the dispatch() calls?
Instead of a bunch of setState(), we dispatch.
dispatch() usually sends an object consisting of:
- the type of action
- the payload (text etc)
An example pseudocode:
dispatch({
type: 'add',
text: inputText
})
Every time we call dispatch() with the action object along with it, we are essentially calling the reducer that we separated into a part on its own. That reducer would then deal with the logic.
Remember this line from the main file?
const [tasks, dispatch] = useReducer(taskReducer, initialTasks)
initialTasks is fed into tasks as the current state.
Whenever we call the reducer, it handles the logic and returns a new state back to the tasks state variable.
What we have essentially done, is to streamline the state logic.
Do you use reducers all the time? Not really. The general rule of thumb is to use reducers if state starts getting complicated. If it stays simple, use useState().
Top comments (0)