useReducer as the foundation of complex state management in React
React has two primary ways of working with state in your application: useState
and useReducer
. useState
is the most popular, and easiest to work with because its the first state management hook that you learn as a beginner. However, useState
can only get you so far.
If you have multiple states that you want to work with in your application, you need to declare several states. Overtime things get complicated real fast, especially if you have degrees of dependency among the states.
To bring order back to the chaos, you don’t need to use external libraries like Redux. Instead, if your state isn’t that complex enough, useReducer
and React’s Context API can help get the job done.
Understanding React’s useReducer
hook
You can start using useReducer like so:
import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState, init?);
It takes two mandatory parameters, a reducer
function and the initial state. It then returns an array with two important things, the current state state
and a dispatch
function.
The third argument init
(an initializer function that should return the initial state) is optional.
If you want to hard code your state beforehand, you can do it by passing it as the second argument, ignoring the third.
const [state, dispatch] = useReducer(reducer, initialState);
If you’re well versed with useState
you can see a familiar pattern going on here.
As a reminder, useState
takes one single parameter (your initial state) and by calling the hook, you get back an array with the current state together with a state setter function (used to update the state).
Here’s an example:
import { useState } from "react";
const [state, setState] = useState(initialState);
Similar to useState
, useReducer
gives you the current state and a function to update the state but with a twist. In useReducer
, the reducer
function is the one responsible for updating state. But to update the state, you have to “dispatch” an action object using the dispatch
function.
The Dispatch Function
If you need to update your state, you must call the dispatch
function. Calling dispatch will also trigger a re-render, which is needed to ensure that your UI displays the current state of the component. To ensure your state is updated, the dispatch function takes an action object as the only argument.
The object specifies the action performed by the user. Conventionally, the object contains a type property that matches with a given type specified in the reducer function.
Typically, you call the dispatch
function after a user does some state-changing action like clicking on a button or after getting response from an API.
const [state, dispatch] = useReducer(reducer, { age: 20 });
function handleClick() {
dispatch({ type: "increment_age" });
}
In the example above, we call the dispatch
function and pass in an action object with a type
of ‘increment_age’. The type
property represents what the user did.
Note: By convention, an action is usually an object with a type property identifying it and, optionally, other properties with additional information. ~ React Docs.
After calling dispatch
with the user’s action, the function will call our reducer
with the specified action object as the argument.
The Reducer Function
According to the official React docs, the reducer specifies how the state gets updated. It takes two parameters, the current state
and an action
.
function reducer(state, action){
switch (action.type) {
case 'increment_age': {
return {
...state,
age: state.age + 1
};
}
...
}
}
The action object usually has a type
property that represents the action dispatched by the dispatch function. You’re free to use an if
statement to specify your action types but a switch
statement is preferred by community convention.
In the sample code above, we’re watching for an action with increment_age
type. Inside the block, we specify how our state will be updated. In the example, we return an object with our current state and modify the age
property by adding one.
Using useReducer
to Declare and Update State
Away with the theory, let’s get real with a real-life example, to help you get a full picture of how the useReducer
hook works and how you can use it in your project.
Let’s say we have an initial state object with two properties, name
and age
. Using useReducer
, let’s declare our initial state:
const [state, dispatch] = useReducer(reducer, { name: "Alvin", age: 100 });
Next, we’ll create the reducer
function with logic for updating our age
property:
function reducer(state, action) {
switch (action.type) {
case "INCREMENT_AGE":
return {
...state,
age: state.age + 1,
};
case "DECREMENT_AGE":
return {
...state,
age: state.age - 1,
};
default:
// add default case in case the action type is unkown
break;
}
}
Assumming we have two buttons displayed on the UI for incrementing and decrementing the age property, like so:
<button onClick={handleAgeIncrement}>Increment age</button>
<button onClick={handleAgeDecrement}>Decrement age</button>
We can call the dispatch function to update the state accordingly when a user clicks on either button by passing in action objects with specific types that match the cases inside our reducer
function.
function handleAgeIncrement() {
dispatch({ type: "INCREMENT_AGE" });
}
function handleAgeDecrement() {
dispatch({ type: "DECREMENT_AGE" });
}
Now each time we click the increment or decrement button, our state will be instantly updated. That’s how you can use useReducer
to manage state in React.
Here’s the full code example:
In case the embed doesn’t load, check it out on codesandbox.
Video walkthrough
Why Should You Care about useReducer
?
useReducer
is specifically important in React because the pattern used to update state is somewhat similar to what you’ll see in Redux, the most popular and robust React state management libraries.
In smaller projects, useReducer
might be all you need. But in larger applications, using dedicated state management libraries is highly recommended.