Hooks were introduced in React starting from version 16.8. Since then, they have allowed for better state and logic management in functional components.
The useReducer
hook is an alternative to useState
for handling complex states. It is based on the reduction pattern, where a "reducer" function receives the current state and an action, and returns a new state. It is a lightweight solution compared to Redux or Flux and is ideal when a component requires multiple state updates based on different actions.
Here is a basic example of useReducer
:
1const initialCounter = () => ({ counter: 0 }); 2const [state, dispatch] = useReducer(counterReducer, initialCounter());
The useReducer
hook takes two arguments:
Reducer: A function that determines how to change the state based on the received action.
Initial state: It can be an object or a function that returns an initial state.
The dispatch
is a function that allows sending actions to modify the state.
The reducer receives the current state and an action to generate a new state:
1function 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 "MULTIPLYBYTWO": 10 return { ...state, counter: state.counter * 2 }; 11 case "RESET": 12 return { ...state, counter: 0 }; 13 default: 14 return state; 15 } 16}
Each action is an object with a type property that defines the operation to perform. If the action does not match any case, the reducer returns the state unchanged.
When a state is simple, useState
is usually sufficient. However, in cases where there are multiple actions affecting the state, useReducer helps keep the logic organized and reusable.
We are used to perceiving 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 numeric counter should look, and also the logic of how it should increment by one each time the "+1" button is pressed.
1export default function Counter() { 2 // Logic ⬇️ 3 const [counter, setCounter] = useState(0); 4 const increment = () => setCounter(counter + 1); 5 6 // View ⬇️ 7 return ( 8 <div className="container"> 9 <h2>State counter</h2> 10 <h3>{counter}</h3> 11 <div className="buttons"> 12 <button onClick={increment}>+1</button> 13 </div> 14 </div> 15 ); 16}
But what if we need to reuse the logic in other components? We could talk about centralized states, but what if I just want to reuse the logic, and each component has its own state? A less practical solution would be to copy and paste or export functions from a separate file and make them work with each component's state 😰. That doesn't sound convenient...
The solution to this problem is useReducer
, which, as its name suggests, the function is to reduce a state and its logic to a reusable unit, allowing it to be exported from a file to the components that need it 💪. This reducer will coexist with the rest of the typical syntax of a React component; you can learn more here.
With useState
, a counter with several actions would look like the following example where we have a counter that not only increments by 1 but also has other options to modify its value.
1export default function CounterUsingState() { 2 const [counter, setCounter] = useState(0); 3 4 return ( 5 <div className="container"> 6 <h2>State counter</h2> 7 <h3>{counter}</h3> 8 <div className="buttons"> 9 <button onClick={() => setCounter(counter + 1)}>+1</button> 10 <button onClick={() => setCounter(counter - 1)}>-1</button> 11 <button onClick={() => setCounter(0)}>0</button> 12 <button onClick={() => setCounter(counter + 10)}>+10</button> 13 <button onClick={() => setCounter(counter * 2)}>x2</button> 14 </div> 15 </div> 16 ); 17}
With useReducer, we can move the logic to a separate file:
1// counterReducer.js 2export const initialCounter = () => ({ counter: 0 }); 3 4export default function counterReducer(state, action) { 5 switch (action.type) { 6 case "INCREMENT": 7 return { ...state, counter: state.counter + 1 }; 8 case "DECREMENT": 9 return { ...state, counter: state.counter - 1 }; 10 case "PLUSTEN": 11 return { ...state, counter: state.counter + 10 }; 12 case "MULTIPLYBYTWO": 13 return { ...state, counter: state.counter * 2 }; 14 case "RESET": 15 return { ...state, counter: 0 }; 16 default: 17 return state; 18 } 19}
And now in the component, we use useReducer
:
1import React, { useReducer } from "react"; 2import counterReducer, { initialCounter } from "./counterReducer"; 3 4export default function CounterUsingReducer() { 5 const [state, dispatch] = useReducer(counterReducer, initialCounter()); 6 7 return ( 8 <div> 9 <h2>Reducer counter</h2> 10 <h3>{state.counter}</h3> 11 <div> 12 <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button> 13 <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button> 14 <button onClick={() => dispatch({ type: "RESET" })}>0</button> 15 <button onClick={() => dispatch({ type: "PLUSTEN" })}>+10</button> 16 <button onClick={() => dispatch({ type: "MULTIPLYBYTWO" })}>x2</button> 17 </div> 18 </div> 19 ); 20}
Now the state logic is completely reusable in other components.
We have seen the advantages of useReducer and know how to extract the logic from our state to a reducer located in an external file that other components can reuse. This does not mean you have to completely discard useState
and only use useReducer
; like everything in programming, it's about using the right tool for the right job. You can learn more about React and its tools in this category.
Reducers are ideal when many functions are associated with the state, and grouping logic and data is convenient. This can occur in a scenario of great complexity or when functions and states need to be reused in multiple components, then you will have the powerful tool of useReducer in your arsenal.