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>
)
}