In this article, we'll take a deep look at the useReducer
hook in React. It can look confusing, especially if you are coming across the hook for the first time. This article breaks down the useReducer
hook concept into understandable bits with both code and real-world examples to enable you grasp its functionality.
If you are having a tough time understanding what the useReducer
is and how it works, this article is for you. However, a good knowledge of how states works is essential to understand what will be covered in this piece. You can read about React states here: State Management In React. You can then join us on the ride to the useReducer
land when you are done. If you are already familiar with states, lets go!
Before we go any further, it is important to note that the useState
and useReducer
hooks are similar in some ways.
How Does useReducer
Compare to the useState
Hook?
- They both involve a current state value, and have a function that triggers a state update and an initial state value passed as an argument.
- The
useReducer
is an alternative to theuseState
hook for managing state in functional components. TheuseReducer
hook is better suited for managing complex state logic whileuseState
is best for simple state changes.
When the state logic becomes too complicated or when you need to handle state changes in a more predictable and manageable way, the useReducer
hook is your best bet.
What is useReducer
?
A useReducer
is a hook in React that allows you add a reducer
to your component. It takes in the reducer function and an initialState
as arguments. The useReducer
also returns an array of the current state
and a dispatch
function.
const [state, dispatch] = useReducer(reducer, initialState);
Let's familiarize ourselves with what the parameters mean:
state
: represents the current value and is set to theinitialState
value during the initial render.dispatch
: is a function that updates the state value and always triggers a re-render, just like the updater function inuseState
.reducer
: is a function that houses all the logic of how the state gets updated. It takes state and action as arguments and returns the next state.initialState
: houses the initial value and can be of any type.
Deep dive into useReducer
Having seen the parts that makes up a useReducer hook, it is time to take a closer look into how it operates.
To use useReducer in your React app, call it at the top level of your component.
import { useReducer } from "react";
We can now use the useReducer hook in our component.
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return(
)
}
To see our useReducer
hook in action, we will build a very simple counter app that increments by 1 when an increment button is clicked and decrements by 1 when a decrement button is clicked.
Firstly, let us take a closer look at the important reducer
function. This function determines how the state gets updated and contains all the logic through which the next state will be calculated.
Basically, reducers house the logic that is usually placed inside of an event handler when using useState
. This makes it easier to read and debug when you are not getting desired results. A quick look at the reducer function can save you the stress.
The reducer function is always declared outside of your component and takes in a current state
and action
as arguments.
function reducer(state, action) {
}
An action
is an object that typically has a type
property which identifies a specific action. Actions describe what happens and contains information necessary for the reducer to update the state.
Conditional statements are used to check the action types and perform a specified operation that would return a new state value. Conditional statements like if
and switch
can be used in reducers.
Dispatch Function
This is a function returned by the useReducer
hook and is responsible for updating state to a new value. The dispatch function takes the action as its only argument.
We can place the dispatch function inside an event handler function. Remember, actions come with a type property so we have to specify when we call the dispatch function. For our counter app, we have two event handlers that increase and decrease the count.
function handleIncrement() {
dispatch({ type: "increment" });
}
function handleDecrement() {
dispatch({ type: "decrement" });
}
Now, we'll go back to our reducer function and use the switch
condition to evaluate the action.type
expression.
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return "Unrecognized command";
}
}
In the above code,
- The
reducer
function takes both the state and action as an argument. - We conditionally check for a specific case of the
action.type
expression string. - If true, a shallow copy of the state is taken by the use of the spread operator and the count value in state is evaluated.
- A new state is returned after evaluation has been completed.
- The
default
serves a fallback when no matching case is found.
The entire logic of our counter app has been done. We can now return our JSX with the state of count
to be displayed on the user interface and the handler functions passed to the onClick
event handler for the buttons.
return (
<>
<h1>Count:{state.count}</h1>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</>
);
Our counter app is set and the count
state will be updated accordingly when the buttons are clicked.
What Happens Behind the Hood?
The action of clicking the button triggers a dispatch
function that sends an information of type
to the reducer function. The dispatching (clicking of the button) causes a re-render of the component. The reducer function conditionally matches the case with the type from the action object and updates the state accordingly after evaluation has taken place.
NOTE: At dispatch, the reducer function still holds the old value. This means that the dispatch function only updates the state variable for the next render. To check this out, we can log the state
and action
arguments to the console before the switch statement:
function reducer(state, action) {
console.log(state, action);
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return "Unrecognized command";
}
}
After clicking the increment button to increase the count twice, here is what is logged to the console:
state and action type logged to the console
The above image shows that, at the first click, there was an action type of increment
made and the initial state value was 0. At the second click, the state value updated to 1, and was displayed as the current count state. I hope you get it now.
Enough of the code gibberish, let's look at a real-world example of the reducer function.
Real-world Reducer Example
Picture a dispatcher that works for an online shopping company going to a warehouse to get the goods/items they would later distribute to the people that ordered them.
The dispatcher identifies themself and performs an action of claiming the goods meant for dispatch to the warehouse manager. The manager goes to a box that contains orders shipped and locates the goods meant to be given to the dispatcher. The manager also logs into the inventory system and does the evaluations before handing over the goods to the dispatcher.
This scenario can also be translated as:
- The dispatcher makes a request for an update or triggers a process like the
dispatch
function. - The dispatcher performs an action of 'claiming goods' like the dispatch
action
with atype
property. - The warehouse manager does the necessary sorting and updating just like the
reducer
function. - The box that houses all the goods is updated depending on how many are cleared for dispatch. This acts like the
state
update.
I hope this real-world scenario makes the entire process clearer to you.
Take a look at the full code once again and digest the process.
import { useReducer } from "react";
function reducer(state, action) {
console.log(state, action);
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return "Unrecognized command";
}
}
const initialState = { count: 0 };
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleIncrement() {
dispatch({ type: "increment" });
}
function handleDecrement() {
dispatch({ type: "decrement" });
}
return (
<>
<h1>Count:{state.count}</h1>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</>
);
}
Benefits of Using the useReducer
Hook
- Helps centralize state logic.
- Makes state transitions predictable.
- Suitable for complex state management.
- Optimizes performance.
Conclusion
We have covered what the useReducer
hook is, how it compares to useState
– similarities and differences, the reducer process and the benefits of using useReducer
.
If you found this article helpful, you can buy me a coffee.
You can also connect with me on LinkedIn .
See you on the next one!