Hooks started to exist in react since version 16.8. Since then, the entire react architecture has been transformed into a series of Hooks that allow you to implement most of the major programming patterns.
The useReducer is a React approach to separate the logic from the view in your components. There are other solutions such as Redux, Flux, Global Context, etc. However, the useReducer has become popular because it is simple to use and maintains a local scope on the data, i.e., despite reusing the functions and code of the components, the data will not be shared with each other.
Separating data from components helps prevent common errors and reuse information and logic in the application.
The first step is to declare a reducer function (in this example it is called counterReducer
) which is defined with 2 parameters: state
which contains the reducer data, and an actions
object which is used to identify the actions we can execute to manipulate the state.
1function counterReducer(state , action = {}) { 2 // Here the reducer receives the current state 3 // then executes the actions 4 // finally it returns the new state 5}
This reducer function is in charge of mutating (or "modifying") the state of your component according to the predefined action types, and should return a new version of the state that completely replaces the previous one at the end of its execution, so be careful and only alter what we need and always return the other values of the state using the destructuring (js destructuring) 🤓.
1function counterReducer(state , action = {}) { 2 // Whatever you do, always return a new state 3 //👍**YES** 4 return { ...state, counter: state.counter + 1 } 5 6 //🚫**NO** 7 //return { counter: state.counter + 1 } 8}
This function is used as the first parameter of the useReducer
hook. As second parameter an object with the initial values of the state must be passed.
The hook call returns an array of two values representing the new state (state
) and the dispatcher: the object that calls the execution of the actions of the reducer logic (actions
).
1 const intialCounter={counter: 0}: 2 const [state, dispatch] = useReducer(counterReducer, initialCounter());
Within the reducer function, the actions
object contains a type
property that tells us which action has been invoked, and we can write logic to mutate the state entirely based on this property.
1export default function counterReducer(state, action = {}) { 2 switch (action.type) { 3 case "INCREMENT": 4 return { ...state, counter: state.counter + 1 }; 5 case "DECREMENT": 6 return { ...state, counter: state.counter - 1 }; 7 case "PLUSTEN": 8 return { ...state, counter: state.counter + 10 }; 9 case "MULTYPLYBYTWO": 10 return { ...state, counter: state.counter * 2 }; 11 case "RESET": 12 return { ...state, counter: 0 }; 13 default: 14 // In the case of having no type it returns the state intact 15 return state; 16 } 17}
In addition to the specified actions, a default
case is placed that is executed when the action type is not defined, for which an error is thrown that interrupts the application. This may seem a little extreme, but it is better to have a visible error and debug it, than to have an application without errors(🐞bugs) but that does not work as it should.
Already with this we can have both the counterReducer
functions and the initialCounter
initial state exported in a file, to be used by any other component 👌.
We are used to perceive the components as the unit that groups the view and the logic for its operation. For example: In the following code there is a Counter
component that has the HTML to define how a counter of numbers should look like and there is also the logic of how it adds a unit each time the "+1" button is pressed.
1export default function Counter() { 2 3 // Logic ⬇️ 4 const [counter, setCounter] = useState(0); 5 const increment = () => setCounter(counter + 1); 6 7 // View ⬇️ 8 return ( 9 <div className="container"> 10 <h2>State counter</h2> 11 <h3>{counter}</h3> 12 <div className="buttons"> 13 <button onClick={increment}>+1</button> 14 </div> 15 </div> 16 ); 17}
What if we need to reuse only the logic in other components? We could consider centralized states, but what if I want to reuse only the logic while leaving every component with its own state? The janky solution would be copying the functions to another file, exporting them from there, and figuring out a way to make them work with every single state component 😰. It doesn't sound convenient...
One solution for this issue is useReducer
, which as its name suggests reduces the state and the logic to a single reusable unit, allowing it to be exported from a file to every component that needs it 💪. This reducer will coexist with the rest of the ordinary component syntax, you can learn more here.
In this example, we have a counter that not only adds one by one but also has other options to modify its value.
To perform all these actions it needs functions for every single one of them, besides the state itself. For that we'll use the classic useState
hook, learn more here.
1export default function CounterUsingState() { 2 const [counter, setCounter] = useState(0); 3 const increment = () => setCounter(counter + 1); 4 const decrement = () => setCounter(counter - 1); 5 const reset = () => setCounter(0); 6 const plusten = () => setCounter(counter + 10); 7 const multiplyByTwo = () => setCounter(counter * 2); 8 9 return ( 10 <div> 11 <h2>State counter</h2> 12 <h3>{counter}</h3> 13 <div> 14 <button onClick={increment}>+1</button> 15 <button onClick={decrement}>-1</button> 16 <button onClick={reset}>0</button> 17 <button onClick={plusten}>+10</button> 18 <button onClick={multiplyByTwo}>x2</button> 19 </div> 20 </div> 21 ); 22}
This works perfectly, but to make this logic reusable and move it to another file, let's convert it into a reducer:
1// counterReducer.js 2export const initialCounter = {counter: 0}; 3export default function counterReducer(state, action = {}) { 4 switch (action.type) { 5 case "INCREMENT": 6 return { ...state, counter: state.counter + 1 }; 7 case "DECREMENT": 8 return { ...state, counter: state.counter - 1 }; 9 case "PLUSTEN": 10 return { ...state, counter: state.counter + 10 }; 11 case "MULTYPLYBYTWO": 12 return { ...state, counter: state.counter * 2 }; 13 case "RESET": 14 return { ...state, counter: 0 }; 15 default: 16 return state; 17 } 18} 19
Now from the component we can import and use the reducer:
1import React, { useReducer } from "react"; 2import counterReducer, { initialCounter } from "./counterReducer"; 3 4export default function CounterUsingReducer() { 5 // Add the hook useReducer, passing as arguments 6 // our reducer function and the initializer, 7 // being both imported from another file. 8 const [state, dispatch] = useReducer(counterReducer, initialCounter); 9 10 return ( 11 <div> 12 <h2>Reducer counter</h2> 13 {/* Now the counter is inside the reducer's state */} 14 <h3>{state.counter}</h3> 15 <div> 16 17 {/* We call the dispatch function passing the type of the action to perform the reducer's logic */} 18 <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button> 19 <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button> 20 <button onClick={() => dispatch({ type: "RESET" })}>0</button> 21 <button onClick={() => dispatch({ type: "PLUSTEN" })}>+10</button> 22 <button onClick={() => dispatch({ type: "MULTYPLYBYTWO" })}>x2</button> 23 </div> 24 </div> 25 ); 26}
For this to work it was necessary to use the state of the reducer and replace the functions for the calls to dispatch
, which runs the logic of the reducer and receives as a parameter the type of action to executer.
We have seen the advantages of useReducer and now we know how to extract the logic and the state to a reducer exported on an external file that can be reused by other components. This doesn't mean you have to dish out useState
entirely and only use useReducer
. Like everything in coding is about using the right tool for the right job. You can learn more about React and the great tools it has in this category
The reducers are ideal when we have a lot of functions associated with a single state, and turns out convenient to group logic and data. This can happen in a scenario of great complexity o when you need to reuse functions and their state across many components, then you will have the mighty tool useReducer in your arsenal.