Editor’s note: This article was last updated on 28 March 2022 to include references to newer tools and frameworks.
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 state 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.
When you consider the complexity of getting started with a third-party library like Redux, which is made much easier with Redux Toolkit, and the excessive amount of boilerplate code needed, managing state with React Hooks and the Context API becomes quite an appealing option. There’s no need to install an external package or add a bunch of files and folders to manage global state in our application.
But, the golden rule still remains. Component state for component state, Redux for application state. 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!
Table of contents
- How does the
useReducer
Hook work? - Creating the initial state lazily
- Building a simple counter app with the
useReducer
Hook useState
vs.useReducer
- When to use the
useReducer
Hook - When not to use the
useReducer
Hook - Conclusion
How does the 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:
- A store: An immutable object that holds the application’s state data
- A reducer: A function that returns some state data, triggered by an action
type
- An action: An object that tells the reducer how to change the state. It must contain a
type
property and can contain an optionalpayload
property
Let’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;
React doesn’t use the (state = initialState, action) => newState
Redux pattern, so the reducer function works a bit differently. The code below shows how you’d create reducers with React:
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 reducer function
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 must have come across this pattern of state management.
Specifying the initial state
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.
Creating the initial state lazily
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, the initFunc
above will reset the initialCount
to 0
on page mount, then return the state object. Notice that this initFunc
is a function, not just an array or object.
The dispatch
method
The 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.
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.
The 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 allows 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>
Bailing out of a dispatch
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.
Building a simple counter app with the useReducer
Hook
Now, 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.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. 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.
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 you can 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.
Declaring state with useState
const [state, setState] = useState('default state');
useState
returns an array that holds the current state value and a setStatemethod
for updating the state.
Declaring state with useReducer
const [state, dispatch] = useReducer(reducer, initialState)
useReducer
returns an array that holds the current state value and a dispatchmethod
that logically achieves the same goal as setState
, updating the state.
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 shape, 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.
When to use the useReducer
Hook
As 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.
A good rule of thumb is that 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, you’re likely better off with useReducer
.
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.
Create a login component
For a better understating 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 function login({ username, password }) { return 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 function login({ username, password }) { return 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.
When not to use the useReducer
Hook
Despite being able to use the useReducer
Hook to handle complex state logic in our app, it’s important to note that there are certainly some scenarios where a third-party state management library like Redux may be a better option.
One simple answer is that you should avoid using Redux or any other third-party state management library until you have problems with vanilla React. If you’re still confused as to whether you need it, chances are, you likely don’t.
Let’s review some specific cases where it makes more sense to use a library like Redux or MobX.
When your application needs a single source of truth
Centralizing your application’s state and logic with a library like Redux makes creating universal application state a breeze because the state from the server can easily be serialized to the client app. Having a single source of truth also makes it easy to implement powerful capabilities like undo/redo features.
When you want a more predictable state
Using a library like Redux helps you write applications that behave consistently when running in different environments. If the same state and action are passed to a reducer, the same result is always produced because reducers are pure functions. Also, state in Redux is read-only, and the only way to change the state is to emit an action, an object describing what happened.
When state-lifting to the top-level component no longer suffices
Using a library like Redux is best when keeping everything in a top-level React component’s state is no longer sufficient.
State persistence
With libraries like Redux and MobX, you can easily save state to localStorage
and make it available to end users, even after page refresh.
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.
Conclusion
In this article, we explored React’s useReducer
Hook in depth, reviewing how it works, when to use it, and comparing it to the useState
Hook.
I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
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.