About the author
Telmo Goncalves is a software engineer with over 13 years of software development experience and an expert in React. He’s currently Engineering Team Lead at Marley Spoon.
Check out more of his work on telmo.is
Back in 2018, Facebook and React introduced React Hooks during their React Conf 2018 with the idea that they could be used to mimic lifecycle methods and state management in functional components.
Since their release, React Hooks have been quickly adopted by the React development community; Hook functions like
useState
and useEffect
are especially popular.
Another Hook which I’ve found to be very useful and relatively underutilized is useReducer
. Using it has some
distinct advantages React developers should keep in mind.
In this article, I’ll discuss when to use useReducer
alongside a simple example illustrating how it’s especially
useful. If you already know your way around Redux, you’re already familiar with how it works.
Note that this article assumes the reader has a general understanding of React.
When to useReducer
Of the Hooks that were introduced by React, two serve as the primary functions for handling state objects and
transitions: useState
and useReducer
.
When dealing with handling or manipulating primitive elements of the state, most developers typically resort to
useState
. However, while useState
is very efficient for simple state handling in a functional component, it
falls short when we need to deal with more complex objects and operations.
Like useState
, useReducer
is used to manage local state in functional components, but it also allows us to deal
with more complex state manipulation by accepting a reducer and an initial state and then returning the updated actual
state with the dispatch
function.
In short, there’s an advantage to using useReducer
when dealing with something in the functional component state which
is more complex than changing primitive values - or - if you want sustain a more predictable and maintainable state.
And then there is useContext
, but for the sake of simplicity, I won’t go into detail about it here.
The Component
For the sake of example, let’s assume we are building an online store. We’ll start with a simple component where we have one single product.
import React from "react";
export default function MainStore() {
return (
<div className="product">
Macbook Pro 13"
<button>Add to Cart</button>
</div>
);
}
Now let’s see how we can use useReducer
. First we set our component initial state. Say we have a quantity of 0
:
const initialState = { quantity: 0 };
Now we need to build a reducer. In this function we are specifying what actions will be performed in it:
function reducer(state, action) {
switch (action.type) {
case "ADD_TO_CART":
return { quantity: state.quantity + 1 };
case "REMOVE_FROM_CART":
return { quantity: state.quantity - 1 };
default:
throw new Error();
}
}
So, we can add actions like ADD_TO_CART
and REMOVE_FROM_CART
.
Pretty simple math here, in case we’re adding to our cart we’ll grab our current state quantity state.quantity
and
add 1
, otherwise, we’ll remove 1
from the quantity
.
useReducer
First, import useReducer
from React:
import React, { useReducer } from "react";
Inside our component, let’s initialize our reducer. Notice that we are using both our reducer
function and the
initialState
set previously:
export default function MainStore() {
const [state, dispatch] = useReducer(reducer, initialState);
// [ ... ]
Great! Let’s do a little debugging to verify expected behavior. Before we actually start using our reducer, let’s see
what’s inside our state
.
Inside the return of the component
, we put:
<pre>
<code>
{JSON.stringify(state)}
</code>
</pre>
The output in the browser should be {"quantity":0}
, the same as our initialState
.
So, we now know that state
stores the state of our reducer, let’s take a look at the dispatch method. As the name
suggests, it’ll be used to dispatch our actions, both ADD_TO_CART
and REMOVE_FROM_CART
.
Without removing our debugger (JSON.stringify
), let’s create a button that will dispatch an action to add the
quantity by 1
:
<button onClick={() => dispatch({ type: "ADD_TO_CART" })}>
Add!
</button>
Amazing! We’re dispatching an action with type ADD_TO_CART
, when this button is clicked it’ll run this piece of
code from our reducer function:
return { quantity: state.quantity + 1 };
Every time the button is clicked, the debugger will increase the quantity
value by 1
.
Similarly, to remove an item from the cart we just need to change the type
of the dispatch
:
<button onClick={() => dispatch({ type: "REMOVE_FROM_CART" })}>
Remove!
</button>
Conclusion
If you are tired of refactoring functional components into class components due to an inability to effectively manage
a complicated state, looking for something that had more functionality than useState
, or just didn’t understand the
advantages of useReducer
, this should be a great starting point to get familiar with it and its application.
Knowing when to use useReducer
over useState
is not always a clear. Remember that leveraging useReducer
may be
the best choice when:
- Dealing with objects or components in state.
- Handling more complex transactions/transitions in state.
- Dealing with state properties that are tied to one another.
- Working inside of larger applications or complex components.
Telmo regularly posts helpful React development tips and guides on Twitter. Be sure to follow him at @telmo