useReducer
Hook useReducer
is like useState
, that it manages states and rerender component whenever the state changes. useReducer
gives more concrete way to handle more complex function. useReducer
has very similar pattern and use with redux
, but it takes away a lot of boiler plate from redux
.
The coding is more complex , but it gives a lot of control to the state. Changes can occur only in listed type of actions.
Using useReducer
Example: CountApp.js
import React, { useReducer } from "react";
const ACTIONS = {
INCREMENT: "increment",
DECREMENT: "decrement"
}
function reducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { count: state.count + 1 };
case ACTIONS.DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
}
function CountApp() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
function increment() {
dispatch({ type: ACTIONS.INCREMENT });
}
function decrement() {
dispatch({ type: ACTIONS.DECREMENT });
}
return (
<div>
<button onClick={decrement}>-</button>
{state.count}
<button onClick={increment}>+</button>
</div>
);
}
export default CountApp;
Breakdown
Hook useReducer
const [state, dispatch] = useReducer(reducer, { count: 0 })
Hook useReducer
takes 2 parameters:
reducer
function that we perform to our state to get a new state
- Initial value. Typically written as an object because typically reducer is used for a complex operation.
useReducer(reducer, 0)
Hook useReducer
returns 2 values:
state
- If initial value is the direct value, use the state name such as
count
dispatch
function: Function that we call to update our state. It will call the reducer
function (first parameter of the hook) given certain parameters.
Function reducer
function reducer(state, action) {
... switch cases...
}
Function reducer takes 2 parameters:
- Current
state
action
, which what we pass to dispatch function
Function reducer codes:
- In typical use, there will be more than one type of action we can choose to perform. We can organize this using
switch
-case
statements.
- Each
case
represent one action type, written as:
case ACTIONS.INCREMENT
- if the action types are organized as an object
case "increment"
- if the action types are left as their string variable names
function reducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { count: state.count + 1 };
case ACTIONS.DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
}
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
Action types
In our example, there are 2 actions: "increment"
and "decrement"
. We can leave the variable name as strings like in the useState
example. However for the typical and more complex use, rather than hardcode the variable here and there, action types should be organized tidily in an object. This will make it very clear what our options are, nothing mysterious, and not prone to typing error.
const ACTIONS = {
INCREMENT: "increment",
DECREMENT: "decrement"
}
Dispatch function
This hook is typically used to handle more than 1 type of action. When we call the dispatch function, what we call it with, will be set as action
variable for function reducer. Current state
will be used as state variable, and dispatch function will return new current state.
function increment() {
dispatch({ type: ACTIONS.INCREMENT });
}
function increment() {
dispatch({ type: "increment" });
}
Comparison with useState
function IncrementUseState() {
const [number, setNumber] = useState(0);
function increment() {
setNumber((prevNumber) => prevNumber + 1);
}
function decrement() {
setNumber((prevNumber) => prevNumber - 1);
}
return (
<div>
<button onClick={decrement}>-</button>
{number}
<button onClick={increment}>+</button>
</div>
);
}
Todo App
Input box
First, we are creating an input
box that's capturing user's typing. Using useState
hook, the text typed in the box is kept as a state
, initialized with ""
. Input box's onChange
is used to detect each keystroke, and setState
function update the state dinamically, each keystrokeas change detected. At this stage, the text are only prepared, but not stored yet, in the actually todo list.
function App() {
const [task, setTask] = useState("");
...
return (
<form ...>
<input
type="text"
value={task}
onChange={(e) => setTask(e.target.value)}
/>
</form>
);
}
Submit form
Place onSubmit
functionality in the form
element. Pressing enter upon typing in the input
box triggers this. Say the onSubmit function name is handleSubmit
.
- Include
dispatch
function call inside this function. Pass the action type for the dispatch as type
and arguments as an propName: value object in payload
.
- Prevent default of refreshing the whole page (then list will be gone). Include
e
as the function parameter.
- Set task back to
""
.
function App() {
...
function handleSubmit(e) {
e.preventDefault();
dispatch({ type: ACTIONS.ADD_TODO, payload: {item: item} });
setItem("");
}
return (
<form onSubmit={handleSubmit}>
<input ... />
</form>
);
}
Create and manage todo list with useReducer
Use useReducer
to create and manage todos
, the array of the task list.
- Use
useReducer
to create and manage todo list array
- Const
ACTIONS
to organize types of actions
- Function
reducer
with state
and action
property
- Organize the type of actions using
switch case
system.
- Don't forget
default
returning state
.
const ACTIONS = {
ADD_TODO: "add-todo",
...
}
function reducer(todos, action) {
switch(action.type) {
case ACTIONS.ADD_TODO:
return ...;
default:
return todos;
}
}
function App(){
const [todos, dispatch] = useReducer(reducer, [])
...
return (
...
)
}
Action 1: Add new task to the todo list
Make case 1 of the reducer action type functioning. In this case, return
an array with:
- Existing array items
- New array item created using a create item function, taking the task name passed as payload as argument. The function is returning an object with at least 2 properties:
- id, generated from date now. Id is important for future identifier for handling array items
- task name, using the value of the passed payload
- additional properties such as complete marking to identify which task is completed.
function reducer(todos, action) {
switch(action.type) {
case ACTIONS.ADD_TODO:
return [...todos, createTask(action.payload.task)];
...
}
}
function createTask(task) {
return {
id: Date.now(),
task: task,
complete: false
}
}
Display the todo list
In most simple way, we can display the list of task items.
function App() {
...
return (
<div>
...
<ul>
{todos.map(e => {
return <li>{e.task}</li>
})}
</ul>
</div>
)
}