Editor’s note: This article was last updated on 10 October 2024 to include new sections on handling API calls with the useReducer
Hook and discuss its role in React 19—including interactions with the use()
and useTransition
Hooks.
useReducer
is one of the additional Hooks that shipped with React v16.8. An alternative to the useState
Hook, useReducer
helps you manage complex state logic in React applications. When combined with other Hooks like useContext
, useReducer
can be a good alternative to Redux, Recoil, or MobX. In certain cases, it is an outright better option.
While Redux, Recoil, and MobX are usually the best options for managing global states in large React applications, more often than necessary, many React developers jump into these third-party state management libraries when they could have effectively handled their state with Hooks.
In this tutorial, we’ll explore the useReducer
Hook in depth, reviewing the scenarios in which you should and shouldn’t use it. Let’s get started!
useReducer
Hook work?The useReducer
Hook is used to store and update states, just like the useState
Hook. It accepts a reducer
function as its first parameter and the initial state as the second. useReducer
returns an array that holds the current state value and a dispatch
function to which you can pass an action and later invoke it. While this is similar to the pattern Redux uses, there are a few differences.
For example, the useReducer
function is tightly coupled to a specific reducer. We dispatch action objects to that reducer only, whereas in Redux, the dispatch function sends the action object to the store. At the time of dispatch, the components don’t need to know which reducer will process the action.
For those who may be unfamiliar with Redux, we’ll explore this concept a bit further. There are three main building blocks in Redux:
type
type
property and can contain an optional payload
propertyLet’s see how these building blocks compare to managing state with the useReducer
Hook. Below is an example of a store in Redux:
import { createStore } from 'redux' const store = createStore(reducer, [preloadedState], [enhancer])
In the code below, we initialize state with the useReducer
Hook:
const initialState = { count: 0 } const [state, dispatch] = useReducer(reducer, initialState)
The reducer function in Redux will accept the previous app state and the action being dispatched, calculate the next state, and return the new object. Reducers in Redux follow the syntax below:
(state = initialState, action) => newState
Let’s consider the following example:
// notice that the state = initialState and returns a new state const reducer = (state = initialState, action) => { switch (action.type) { case 'ITEMS_REQUEST': return Object.assign({}, state, { isLoading: action.payload.isLoading }) case ‘ITEMS_REQUEST_SUCCESS': return Object.assign({}, state, { items: state.items.concat(action.items), isLoading: action.isLoading }) default: return state; } } export default reducer;
useReducer
doesn’t use the (state = initialState, action) => newState
Redux pattern, so its reducer function works a bit differently. The code below shows how you’d create reducers with React’s useReducer
:
function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } }
Below is an example of an action that can be carried out in Redux:
{ type: ITEMS_REQUEST_SUCCESS, payload: { isLoading: false } } // action creators export function itemsRequestSuccess(bool) { return { type: ITEMS_REQUEST_SUCCESS, payload: { isLoading: bool, } } } // dispatching an action with Redux dispatch(itemsRequestSuccess(false)) // to invoke a dispatch function, you need to pass action as an argument to the dispatch function
Actions in useReducer
work in a similar way:
// not the complete code switch (action.type) { case 'increment': return {count: state.count + 1}; default: throw new Error(); } // dispatching an action with useReducer <button onClick={() => dispatch({type: 'increment'})}>Increment</button>
If the action type in the code above is increment
, our state object is increased by 1
.
The JavaScript reduce()
method executes a reducer function on each element of the array and returns a single value. The reduce()
method accepts a reducer function, which itself can accept up to four arguments. The code snippet below illustrates how a reducer works:
const reducer = (accumulator, currentValue) => accumulator + currentValue; [2, 4, 6, 8].reduce(reducer) // expected output: 20
In React, useReducer
essentially accepts a reducer function that returns a single value:
const [count, dispatch] = useReducer(reducer, initialState);
The reducer function itself accepts two parameters and returns one value. The first parameter is the current state, and the second is the action. The state is the data we are manipulating. The reducer function receives an action, which is executed by a dispatch
function:
function reducer(state, action) { } dispatch({ type: 'increment' })
The action is like an instruction you pass to the reducer function. Based on the specified action, the reducer function executes the necessary state update. If you’ve used a state management library like Redux before, then you’ve probably come across this state management pattern.
The initial state is the second argument passed to the useReducer
Hook, which represents the default state:
const initialState = { count: 1 } // wherever our useReducer is located const [state, dispatch] = useReducer(reducer, initialState, initFunc)
If you don’t pass a third argument to useReducer
, it will take the second argument as the initial state. The third argument, which is the init
function, is optional. This pattern also follows one of the golden rules of Redux state management: the state should be updated by emitting actions. Never write directly to the state.
However, it’s worth noting that the Redux state = initialState
convention doesn’t work the same way with useReducer
because the initial value sometimes depends on props.
In programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed.
As mentioned above, useReducer
can accept a third parameter, which is an optional init
function for creating the initial state lazily. It lets you extract logic for calculating the initial state outside of the reducer function, as seen below:
const initFunc = (initialCount) => { if (initialCount !== 0) { initialCount=+0 } return {count: initialCount}; } // wherever our useReducer is located const [state, dispatch] = useReducer(reducer, initialCount, initFunc);
If the value is not 0
already, initFunc
above will reset initialCount
to 0
on page mount, then return the state object. Notice that this initFunc
is a function, not just an array or object.
dispatch
in ReactThe dispatch
function accepts an object that represents the type of action we want to execute when it is called. Basically, it sends the type of action to the reducer function to perform its job, which, of course, is updating the state. Think of dispatch
as a messenger that delivers instructions (actions) to the state manager (reducer).
The action to be executed is specified in our reducer function, which in turn, is passed to the useReducer
. The reducer function will then return the updated state.
The actions that will be dispatched by our components should always be represented as one object with the type
and payload
key, where type
stands as the identifier of the dispatched action and payload
is the piece of information that this action will add to the state. dispatch
is the second value returned from the useReducer
Hook and can be used in our JSX to update the state:
// creating our reducer function function reducer(state, action) { switch (action.type) { // ... case 'reset': return { count: action.payload }; default: throw new Error(); } } // wherever our useReducer is located const [state, dispatch] = useReducer(reducer, initialCount, initFunc); // Updating the state with the dispatch functon on button click <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>
Notice how our reducer function uses the payload that is passed from the dispatch
function. It sets our state object to the payload, i.e., whatever the initialCount
is. Note that we can pass the dispatch
function to other components through props, which alone is what allow us to replace Redux with useReducer
.
Let’s say we have a component that we want to pass as props to our dispatch function. We can easily do that from the parent component:
<Increment count={state.count} handleIncrement={() => dispatch({type: 'increment'})}/>
Now, in the child component, we receive the props, which, when emitted, will trigger the dispatch function and update the state:
<button onClick={handleIncrement}>Increment</button>
If the useReducer
Hook returns the same value as the current state, React will bail out without rendering the children or firing effects because it uses the Object.is
comparison algorithm.
useReducer
HookNow, let’s put our knowledge to use by building a simple counter app with the useReducer
Hook:
import React, { useReducer } from 'react'; const initialState = { count: 0 } // The reducer function function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 } case 'decrement': return { count: state.count - 1 } case 'reset': return {count: state.count = 0} default: return { count: state.count } } } const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState) return ( <div> Count: {state.count} <br /> <br/> <button onClick={() => dispatch({ type: 'increment' })}>Increment</button> <button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button> <button onClick={() => dispatch({ type: 'reset'})}>Reset</button> </div> ); }; export default Counter;
First, we initialize the state with 0
, then we create a reducer function that accepts the current state of our count as an argument and an action. The state is updated by the reducer based on the action type. increment
, decrement
, and reset
are all action types that, when dispatched, update the state of our app accordingly.
To increment the state count const initialState = { count: 0 }
, we simply set count
to state.count + 1
when the increment
action type is dispatched.
useState
vs. useReducer
useState
is a basic Hook for managing simple state transformation, and useReducer
is an additional Hook for managing more complex state logic. However, it’s worth noting that useState
uses useReducer
internally, implying that you could use useReducer
for everything you can do with useState
.
However, there are some major differences between these two Hooks. With useReducer
, you can avoid passing down callbacks through different levels of your component. Instead, useReducer
allows you to pass a provided dispatch
function, which in turn will improve performance for components that trigger deep updates.
However, this does not imply that the useState
updater function is newly called on each render. When you have a complex logic to update state, you simply won’t use the setter directly to update state. Instead, you’ll write a complex function, which in turn would call the setter with updated state.
Therefore, it’s recommended to use useReducer
, which returns a dispatch
method that doesn’t change between re-renders, and lets you have the manipulation logic in the reducers.
It’s also worth noting that, with useState
, the state updater function is invoked to update state, but with useReducer
, the dispatch
function is invoked instead, and an action with at least a type is passed to it. Now, let’s take a look at how both Hooks are declared and used.
useState
useState
returns an array that holds the current state value and a setState
method for updating the state:
const [state, setState] = useState('default state');
useReducer
useReducer
returns an array that holds the current state value and a dispatch
method that logically achieves the same goal as setState
, updating the state:
const [state, dispatch] = useReducer(reducer, initialState)
Updating state with useState
is as follows:
<input type='text' value={state} onChange={(e) => setState(e.currentTarget.value)} />
Updating state with useReducer
is as follows:
<button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>
We’ll discuss the dispatch
function in greater depth later in the tutorial. Optionally, an action object may also have a payload
:
<button onClick={() => dispatch({ type: 'decrement', payload: 0})}>Decrement</button>
useReducer
can be handy when managing complex state shapes, for example, when the state consists of more than primitive values, like nested arrays or objects:
const [state, dispatch] = useReducer(loginReducer, { users: [ { username: 'Philip', isOnline: false}, { username: 'Mark', isOnline: false }, { username: 'Tope', isOnline: true}, { username: 'Anita', isOnline: false }, ], loading: false, error: false, }, );
It’s easier to manage this local state because the parameters depend on each other, and all the logic could be encapsulated into one reducer.
useReducer
HookAs your application grows in size, you’ll most likely deal with more complex state transitions, at which point you’ll be better off using useReducer
. useReducer
provides more predictable state transitions than useState
, which becomes more important when state changes become so complex that you want to have one place to manage state, like the render function.
useReducer
is the better option when you move past managing primitive data, i.e., a string, integer, or Boolean, and instead must manage a complex object, like with arrays and additional primitives.
If you’re more of a visual learner, the video below gives a detailed explanation with practical examples of when to use the useReducer
Hook:
Why I Love useReducer
React Hooks have been out for a couple of months now. I’ve had the chance to play with them during that time and recently found an amazing example of where the useReducer Hook really shines. I was so excited about how much stronger useReducer made my components that I had to share it with you.
For a better understanding of when to use useReducer
, let’s create a login component and compare how we’d manage state with both the useState
and useReducer
Hooks.
First, let’s create the login component with useState
:
import React, { useState } from 'react'; export default function LoginUseState() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoading, showLoader] = useState(false); const [error, setError] = useState(''); const [isLoggedIn, setIsLoggedIn] = useState(false); const onSubmit = async (e) => { e.preventDefault(); setError(''); showLoader(true); try { await new Promise((resolve, reject) => { setTimeout(() => { if (username === 'ejiro' && password === 'password') { resolve(); } else { reject(); } }, 1000); }); setIsLoggedIn(true); } catch (error) { setError('Incorrect username or password!'); showLoader(false); setUsername(''); setPassword(''); } }; return ( <div className='App'> <div className='login-container'> {isLoggedIn ? ( <> <h1>Welcome {username}!</h1> <button onClick={() => setIsLoggedIn(false)}>Log Out</button> </> ) : ( <form className='form' onSubmit={onSubmit}> {error && <p className='error'>{error}</p>} <p>Please Login!</p> <input type='text' placeholder='username' value={username} onChange={(e) => setUsername(e.currentTarget.value)} /> <input type='password' placeholder='password' autoComplete='new-password' value={password} onChange={(e) => setPassword(e.currentTarget.value)} /> <button className='submit' type='submit' disabled={isLoading}> {isLoading ? 'Logging in...' : 'Log In'} </button> </form> )} </div> </div> ); }
Notice how we are dealing with all these state transitions, like username
, password
, isLoading
, error
, and isLoggedIn
, when we really should be more focused on the action that the user wants to take on the login component.
We used five useState
Hooks, and we had to worry about when each of these states is transitioned. We can refactor the code above to use useReducer
and encapsulate all our logic and state transitions in one reducer function:
import React, { useReducer } from 'react'; function loginReducer(state, action) { switch (action.type) { case 'field': { return { ...state, [action.fieldName]: action.payload, }; } case 'login': { return { ...state, error: '', isLoading: true, }; } case 'success': { return { ...state, isLoggedIn: true, isLoading: false, }; } case 'error': { return { ...state, error: 'Incorrect username or password!', isLoggedIn: false, isLoading: false, username: '', password: '', }; } case 'logOut': { return { ...state, isLoggedIn: false, }; } default: return state; } } const initialState = { username: '', password: '', isLoading: false, error: '', isLoggedIn: false, }; export default function LoginUseReducer() { const [state, dispatch] = useReducer(loginReducer, initialState); const { username, password, isLoading, error, isLoggedIn } = state; const onSubmit = async (e) => { e.preventDefault(); dispatch({ type: 'login' }); try { await new Promise((resolve, reject) => { setTimeout(() => { if (username === 'ejiro' && password === 'password') { resolve(); } else { reject(); } }, 1000); }); dispatch({ type: 'success' }); } catch (error) { dispatch({ type: 'error' }); } }; return ( <div className='App'> <div className='login-container'> {isLoggedIn ? ( <> <h1>Welcome {username}!</h1> <button onClick={() => dispatch({ type: 'logOut' })}> Log Out </button> </> ) : ( <form className='form' onSubmit={onSubmit}> {error && <p className='error'>{error}</p>} <p>Please Login!</p> <input type='text' placeholder='username' value={username} onChange={(e) => dispatch({ type: 'field', fieldName: 'username', payload: e.currentTarget.value, }) } /> <input type='password' placeholder='password' autoComplete='new-password' value={password} onChange={(e) => dispatch({ type: 'field', fieldName: 'password', payload: e.currentTarget.value, }) } /> <button className='submit' type='submit' disabled={isLoading}> {isLoading ? 'Logging in...' : 'Log In'} </button> </form> )} </div> </div> ); }
Notice how the new implementation with useReducer
has made us more focused on the action the user is going to take. For example, when the login
action is dispatched, we can see clearly what we want to happen. We want to return a copy of our current state, set our error
to an empty string, and set isLoading
to true:
case 'login': { return { ...state, error: '', isLoading: true, }; }
The beautiful thing about our current implementation is that we no longer have to focus on state transition. Instead, we are keen on the actions to be executed by the user.
Let’s extend the existing example to show how to handle API Calls with useReducer
by including an actual API call and update a table with the response data:
import React, { useReducer, useEffect } from 'react'; import axios from 'axios'; const initialState = { users: [], loading: false, error: null }; function userReducer(state, action) { switch (action.type) { case 'FETCH_START': return { ...state, loading: true, error: null }; case 'FETCH_SUCCESS': return { ...state, loading: false, users: action.payload }; case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; default: return state; } } function UserList() { const [state, dispatch] = useReducer(userReducer, initialState); useEffect(() => { const fetchUsers = async () => { dispatch({ type: 'FETCH_START' }); try { const response = await axios.get('https://api.example.com/users'); dispatch({ type: 'FETCH_SUCCESS', payload: response.data }); } catch (error) { dispatch({ type: 'FETCH_ERROR', payload: error.message }); } }; fetchUsers(); }, []); const { users, loading, error } = state; if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <table> <thead> <tr> <th>ID</th> <th>Name</th> <th>Email</th> </tr> </thead> <tbody> {users.map(user => ( <tr key={user.id}> <td>{user.id}</td> <td>{user.name}</td> <td>{user.email}</td> </tr> ))} </tbody> </table> ); }
The above example shows how to use useReducer
to manage the state of an API call, including its loading and error states, and how to update a table with the fetched data.
Remember, the choice between useState
and useReducer
often comes down to the complexity of your state logic and personal preference. Don’t be afraid to start with useState
and refactor to useReducer
if you find your state logic becoming too complex.
useReducer
HookDespite being able to use the useReducer
Hook to handle complex state logic in our app, it’s important to note that there are some scenarios where a third-party state management library like Redux may be a better option:
With all these benefits, it’s also worth noting that using a library like Redux, as opposed to using pure React with useReducer
, comes with some tradeoffs. For example, Redux has a hefty learning curve that is minimized by using Redux Toolkit, and it’s definitely not the fastest way to write code. Rather, it’s intended to give you an absolute and predictable way of managing state in your app.
useReducer
The following are the most common issues you might encounter when using useReducer
. They are mostly caused by developer errors and not the Hook issue:
state.count += 1; return state;
This can lead to errors. The solution would be using the spread operation or other immutable techniques, so that the initial state isn’t affected:
return {…state, count: state.count + 1 }
While useReducer
didn’t receive any update in the new React 19 release, it’s important to understand its place in the broader React ecosystem.
useReducer
With the introduction of React Server Components in React 19, it’s worth noting that useReducer
(like all Hooks) can only be used in client components. Server components are stateless and can’t use Hooks.
useReducer
and use()
React 19 introduces the new use()
Hook, which can be used to consume promises or context. While not directly related to useReducer
, the use()
Hook can complement useReducer
when dealing with asynchronous data in your reducers.
import { use, useReducer } from 'react'; // ... const [state, dispatch] = useReducer(reducer, initialState); function fetchUser() { dispatch({ type: 'FETCH_USER_START' }); try { const user = use(fetchUserData(userId)); dispatch({ type: 'FETCH_USER_SUCCESS', payload: user }); } catch (error) { dispatch({ type: 'FETCH_USER_ERROR', payload: error.message }); } } // ...
useReducer
and useTransition
HookWith the introduction of useTransition
in React 18 and its enhanced capabilities in React 19, we can combine it with useReducer
to create more concise logic, especially when dealing with data mutations and asynchronous operations.
useTransition
allows us to mark updates as transitions, which tells React that they can be interrupted and don’t need to block the UI. This is particularly useful when combined with useReducer
for handling complex state updates that might involve API calls or other time-consuming operations.
In this article, we explored React’s useReducer
Hook, reviewing how it works, when to use it, and comparing it to the useState
Hook.
Remember, the goal is not to use useReducer
everywhere, but to use it where it makes your code clearer and more manageable. As with all patterns in software development, the key is to understand the trade-offs and choose the right tool for the job.
I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
9 Replies to "A guide to the React useReducer Hook"
Very Helpful Article. It goes well in depth about useReducer and differences between useState. Thanks!
Amazing explanation about useReducer. Thanks you so much!!!
I’m glad you find it useful
Amazing effort to put a in depth explanation with examples. Thank You!!
Well explained. Thank you..
Really good explanation, thanks! Just starting to learn react and on top of a lot of new stuff to learn there is more new stuff to learn with the introduction to the Hooks.
I am currently stuck. I am using useReducer because I have form with multiple inputs. I capture the inputs and call an API. I am getting the results back.
The part I am stuck on is how to get that data into a table of sorts. But before I can do that I need to update the state with the data I get back (I think). I am having a hard time finding how to do that. Most examples just have everything on one page: the reducer, the axios call, the update to one field because they are using useState, and just display the results in a console log. That’s great but who does that? #1) Just call a get API on page load and #2) Just plop the data into the console log. While informative and helps with some understanding, it is this last piece of the puzzle that I am stuck on. I am a little frustrated at the moment. If you know of a site that shows this last piece of the puzzle of if you have an example somewhere please let me know. Thanks for your time and sharing this info for newbs!
I had to alter the code for the code to work: while the await login function is declared, it is not called as a method. So refactoring to extract the | function login… | (which now you can make available to both the state and reducer examples):
const handleOnSubmit = async (e) => {
e.preventDefault();
setError(”);
showLoader(true);
function login(u, p) {
return new Promise((resolve, reject) => {
console.log(‘fuc’);
setTimeout(() => {
if (u === ‘username’ && p === ‘password’) {
resolve();
} else {
reject();
}
}, 1000);
});
}
try {
await login(username, password)
setIsLoggedIn(true);
} catch (error) {
setError(‘Incorrect username or password!’);
showLoader(false);
setUsername(”);
setPassword(”);
}
}
I’ve got a useReducer that receives a payload, calls a function that returns the actual “payload” that updates teh context. Should those functions be part of the reducer itself, or global? wrapped in useCallback?
Wonderful article on the useReducer hook for state management. Found it very informative and helpful.