Welcome to A Look at React Hooks, a beginner-friendly series on React Hooks. In this article, let's look at the useReducer
Hook.
What is useReducer?
For those who are familiar with Redux, the useReducer
Hook allows React to access reducer functions for state management. If you are not from a Redux background, a simple way to describe this Hook is that it is an alternative to the useState
Hook. The difference is that it allows for more complex logic and state updates that contain multiple sub-values.
This Hook typically accepts 2 arguments: a reducer function and an initialState. Optionally, it accepts a 3rd argument, an init
function which we will discuss later.
So the useReducer
Hook will initialize as:
useReducer(reducer, initialState)
Similar to useState
, it returns an array of 2 values which can be destructured as the current value of the state and a dispatch function.
In summary, the Hook can be initialized as:
const [state, dispatch] = useReducer(reducer, initialState)
The reducer function
Before we learn how to use the Hook, let's learn about one of its arguments: the reducer function. A 'reducer' is basically a function that accepts a state
and an action
, then returns a value.
For example:
function reducer(state, action) {
return state + action;
}
This is how the Hook can update the values of its state. The action
determines how the value of state
will change. In the example above, the new value of the state
will be the sum of the initial state
and action
.
The dispatch function
And now you may ask: how do we use pass an action to this Hook's reducer function?
Well, the dispatch
function is one responsible for that. It dispatches the action
to be used to update the value of state
.
For example, here we have a simple button that dispatches the action
of value 1. The reducer function that adds 1 to its current state and returns the new value.
<button onClick={() => dispatch(1)}>
Add
</button>
Note that in Redux, the dispatch
function usually dispatches an object in the format:
{type: 'Add', payload: 1}
Where 'type' is the description of the action and 'payload' is the value. But for useReducer
, it is optional to stick to this convention.
An Example
Now that we have learnt the basics, let's see how we can implement everything we learned in an example. Let's say we want to make a simple counter app. We can increase or decrease the count state, or even reset it to zero.
As usual, we first import the Hook.
import React, { useReducer } from 'react';
Then we proceed as follows:
- Set an initialState to 0.
- Write the reducer function. Depending on the 'type' of the action, we increment, decrement or reset the state.
- Initialize the useReducer Hook
- Add the relevant dispatch function to buttons
In code, it will look like:
//1.
const initialState = 0;
//2.
function reducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return 0;
default:
throw new Error();
}
}
function App() {
//3.
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1> Count: {state}</h1>
<!-- 4. -->
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
As you can see below, the app works perfectly!
Important Things to Know
Now that we know how to use this Hook, it is important to know some details.
1. Local 'Store'
If you know Redux, you know that it has a centralized 'store' where the app can access variables from any of its components.
However, keep in mind that useReducer
only stores data locally within its component.
2. Reducers are pure
Notice that the reducer function must be a pure function. This means that the function returns the same value if the same arguments are passed and there are no side effects.
Let's say we alter our counter app example reducer function to directly mutate the value of state instead of returning as a new value. The app will no longer work.
function reducer(state, action) {
switch (action.type) {
case "increment":
return state++; //directly increasing state
case "decrement":
return state--; //directly decreasing state
case "reset":
return 0;
default:
throw new Error();
}
}
3. Lazy Initialization
As mentioned earlier, the Hook optionally takes in a 3rd argument: init. init
is a function that sets the initialState with init(initialArg)
.
Back to our counter app example, we can use the init
function to set our initial state as follows:
function init(initialState) {
return initialState;
}
Then we edit our reducer function to set state to its initial value when 'type' is "reset".
function reducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return init(action.payload); //used init instead of hard code as 0
default:
return;
}
}
Finally, we make some changes to our App component and Hook.
function App() {
//add init as the 3rd argument
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<div>
<h1> Count: {state}</h1>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<!--Add a payload property with initialState as the value-->
<button onClick={() =>
dispatch({ type: "reset", payload: initialState})}>Reset</button>
</div>
);
}
Conclusion
And that's the gist of this Hook! Thanks for reading this article. I hope it was helpful for React beginners. Please feel free to ask questions in the comments below. Ultimately, practising and building projects with this Hook will help you to pick it up faster.
The next Hook in this series will be: useCallback
. Stay tuned and cheers!