Every week, we pick a real-life project to build your portfolio and get ready for a job. All projects are built with ChatGPT as co-pilot!
Start the ChallengeA tech-culture podcast where you learn to fight the enemies that blocks your way to become a successful professional in tech.
Listen the podcastThe hooks were launched on version 16.8 of React. Since then all the architecture of react has transformed into a series of hooks that allow the implementation of most of the most important coding design patterns.
useReducer is a proposal from React to separate the logic from the view of your components. There are other solutions like Redux, Flux, Global Context, etc; however, useReducer is easy to use and keeps the data in a local scope, which means that even when the components are reusing the functions, they don't share data.
The first step is to declare a reducer function wich is defined with 2 parameters: The state
that has all the data of the reducer, and an actions
object that is used to identify the actions can be performed to manipulate the state.
1function counterReducer(state , action = {}) { 2 // Here the reducer receives the state and execute the actions 3 // at last, it returns a new state. 4}
This reducer function is in charge of mutate (modify) the state of your component according to the predefined action types, and it must return the new version of the state which replaces entirely the previous one at the end of the execution, which is why you must be careful to only write what you need and keep all the other values intact by using 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 meant to be used as the first parameter of the useReducer
hoook. As a second parameter, it receives a function that returns an object with the initial values of the state.
The hook call returns an array of two values that represents the state (state
) and the dispatcher: The object that call the executions of actions that perform the logic of the reducer (actions
).
1 const intitialCounter = () => ({counter: 0}); 2 const [state, dispatch] = useReducer(counterReducer, intitialCounter());
Inside the reducer, the object actions
contain the property type
that indicates which action has been invoked, and we can write the logic to mutate the state entirely.
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}
With this, we can have the functions counterReducer
and intitialCounter
exported from a file, to be utilized by another 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 intitialCounter = () => ({ 3 counter: 0 4}); 5export default function counterReducer(state, action = {}) { 6 switch (action.type) { 7 case "INCREMENT": 8 return { ...state, counter: state.counter + 1 }; 9 case "DECREMENT": 10 return { ...state, counter: state.counter - 1 }; 11 case "PLUSTEN": 12 return { ...state, counter: state.counter + 10 }; 13 case "MULTYPLYBYTWO": 14 return { ...state, counter: state.counter * 2 }; 15 case "RESET": 16 return { ...state, counter: 0 }; 17 default: 18 return state; 19 } 20} 21
Now from the component we can import and use the reducer:
1import React, { useReducer } from "react"; 2import counterReducer, { intitialCounter } 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, intitialCounter()); 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.