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 useReducer
hook receives as the first parameter a function that defines the reducer
and will return an array of two values that represents the state of the reducer (state
) and the object that allows dispatching the actions that perform the logic of the reducer (actions
). As a second parameter, it receives a function that returns an object with the initial values of the state.
1 const intitialCounter = () => ({counter: 0}); 2 const [state, dispatch] = useReducer(counterReducer, intitialCounter());
At the same time, the reducer function itself is defined with 2 parameters: The state
that has all the data of the reducer, and an object that is used to identify the actions that must be performed inside it (which we'll call actions
).
1function counterReducer(state , action = {}) { 2 // Here the reducer receives the state and execute the actions 3}
This reducer function will be executed on every action call 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 π€.
πYES
1return { ...state, counter: state.counter + 1 }
π«NO
1return { counter: state.counter + 1 }
Inside the reducer, the object actions
contain the property type
that indicates to us which action has been invoked, and we can write the logic based on it.
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 π.
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.