Understanding the React useReducer Hook

Read it in 11 Mins

Last updated on
13th Jul, 2022
Published
13th Jan, 2021
Views
4,690
Understanding the React useReducer Hook

The introduction of Hooks in React brought up a new way of writing and thinking about React applications. One of the most popular Hooks among developers so far is useReducer, which allows us to handle some complex state manipulations and updates, and this is what we’ll be learning about in this article.

In React there are two main hooks that are used for state management, and we all know them. These are:

  • useState, and
  • useReducer

You might have heard or used React.useState hook, and if not you can directly read React’s official documentation about Hooks here.

Have you ever found any difficulty in choosing a state management library to manage and handle a global state in a React app? Or, what if you need to manage more complicated data structures or perform any side effects?Thinking over these concepts is quite tricky and time-consuming.

Well, this is pretty much what we are going to learn and cover in this article, so read on! Also, do not forget to read about React Native Interview Questions.

Prerequisites

In order to get the full benefits of this article, I’d recommend you to double check this prerequisite list and set up the environment as suggested.

  • Npm or Yarn installed
  • Node.js version >= 12.x.x installed
  • create-react-app cli installed or use npx
  • Basics of React Hooks, (if not kindly read this first)

What is a Reducer, anyway?

What is the first thing that comes up in your mind when you hear the term reducer in React?

For me, I am always reminded of JavaScript's Array.reducer() function.

The only job of a reducer is to Reduce!

We all know that the original reduce() method in JavaScript executes a given function for each value of an array  considering from left-to-right. And, here React.useReducer in fact, is like a mini Redux reducer.  

A reducer is basically a function which determines a change to an application’s state. It takes the help of an action by using it to receive and determine that particular change.

In case you want to learn React online we have a lot of options, like Redux, that can help to manage the application’s state changes in a single store. Here we will learn about how we can make use of a reducer to manage shared state in an application.

Reducer is one of the new custom Hooks introduced in React since v16.8. It allows you to update parts of your component’s state when certain actions are dispatched, and it is very similar to how Redux works.

The reducer in ‘useReducer’ comes from Redux, which in turn borrowed it from JavaScript’s Array.reduce().

It carries an initial state and a reducer function as its arguments and then provides a state variable and a dispatch function to enable us to update the state. If you’re familiar with how Redux updates the store through reducers and actions, then you already know how useReducer works. And if not, then also we will learn about useReducer functionality in core.

So basically, what is useReducer Hook?

The useReducer hook is used for complex state manipulations and state transitions.  Just like the other React hooks, we can import useReducer from react as shown in the below snippet:

import React, { useReducer } from 'react';

React.useReducer is a React hook function that accepts a reducer function, and an initial state.  

const [state, dispatch] = useReducer(reducer, initialState);

This hook function returns an array with 2 values. The first one is the state value, and the second value is the dispatch function which is further used to trigger an action with the help of array destructuring.  

In the context of React, here’s what a typical useReducer pattern looks like:

JSX:

const reducer = function (currentState, action) { // Make a new state based on the current state and action return newState}const [state, dispatch] = useReducer(reducer, initialValue)// example usage: dispatch({type: "SOMETHING_HAPPENED"}) // Or with an optional "data": dispatch({type: "SOMETHING_HAPPENED", data: newData})

Note: The “state” can be of  any kind. It doesn’t have to be an object always. It could be a number, or an array, or anything else.

Pretty cool? Now let’s move further and understand how to use useReducer.

How to use the useReducer Hook?

Earlier we saw the basic useReducer which takes up two arguments: initial state and a reducer function. But useReducer hook actually takes up one more argument including reducer and initial state which is >>> a function to load the initial state lazily.

This is usually helpful when we want the initial state to be different depending on some situation and instead of using our actual state, we could create the initial state anywhere, perhaps dynamically, and it will override the initial state.

Syntax for the third argument is:

const [state, dispatch] = useReducer(reducer, initialArgs, init); 

Let’s now understand step by step how to use useReducer and understand what’s happening behind the deck.

Consider the following classic example of code to see the practical usage of useReducer:

// Let’s begin to define the initial state of the component's state

const initialState = { count: 0 } // Here is a function will determine how the state is updated

function reducer(state, action) {   switch(action.type) { case 'INCREMENT':  return { count: state.count + 1 } case 'DECREMENT': return { count: state.count - 1 } case 'REPLACE': return { count: action.newCount } case 'RESET': return { count: 0 } default: return state } } // Now inside our component, we can initialize the state like below
const [state, dispatch] = useReducer(reducer, initialState);

Explanation:
In the above code snippet , we have  

  • first defined an initial state for our component
  • added a reducer function that updates that state depending on the action dispatched and,
  • we have finally initialized the state for our component.

Now as promised earlier, you don’t require an understanding of Redux to understand this concept. So let’s break down everything and see what’s happening.

The initialState variable

This is the default value of our component’s state when it gets mounted for the first time inside the application.

The reducer function

The next step is to update the component’s state when some actions occur. This function tells what the state should contain depending on the action. It returns an object, which is then used to replace / change the state.

It takes in two arguments which are a state and an action; wherein state is nothing but your application’s current state, and the action is an object which contains the details of all the actions currently happening.

An action may typically look like this:

const replaceAction = { type: 'REPLACE', newCount: 10, }

It usually contains a type: which denotes what type of action it is. An action can also contain more than one data, which can also be the new value to be updated in the state.

Dispatching an action

Now after understanding about the reducer and how it determines the next state for our component through actions, let’s see how actions are dispatched.

Dispatch is just like a function which we can pass around to other components through props.

You must have noticed that useReducer returns two values in an array. The first one is the state object, and the second one is a function called dispatch. This is what is used to dispatch an action.

For example, if we want to dispatch replaceAction defined in the above example, we’d do something like this:

dispatch(replaceAction) // or dispatch({ type: 'REPLACE', newCount: 10, })

Summarising a bit here, hence for using useReducer we need the following:

  • Defining an initial state
  • Providing a function which contains actions that can update the state.
  • Trigger useReducer to dispatch the updated state

Understanding useReducer with examples

Let’s play with some real examples now to understand the concept better:

Example:1 Simple Classic Counter Example

Let’s imagine our Component is Counter. useReducer here in the below code accepts as arguments a reducer function, and an initial state value.  

const Counter = () => { const [state, dispatch] = useReducer(reducer, 0)}

In this case our state is an integer, which starts from 0:

The reducer is a function that takes the current state and an action, which can be a value of any type you want. In this example it’s a string:

const reducer = (state, action) => { switch (action) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: throw new Error() } }

We can use JSX to make this simple component work as below:

const Counter = () => {const [count, dispatch] = useReducer(reducer, 0) return ( <> Counter: {count} <button onClick={() => dispatch('INCREMENT')}>+</button> <button onClick={() => dispatch('DECREMENT')}>-</button> </> ) } 

This state can be an object with 'n’ number of properties, but different actions only change one property at a time.

Putting up all this together, our Babel will look like:

const { useReducer } = React const reducer = (state, action) => { switch (action) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: throw new Error() } } const Counter = () => { const [count, dispatch] = useReducer(reducer, 0) return ( <> Counter: {count} <button onClick={() => dispatch('INCREMENT')}>+</button> <button onClick={() => dispatch('DECREMENT')}>-</button> </>  ) } ReactDOM.render(<Counter />, document.getElementById('app')) 

You should get the below output:

Example2: Let’s see another TODO example. To show list of items:

It was not possible to change the state of an item with the handler function earlier. But however, we can now do so, for example if we need to make the list of items stateful we can do so by using them as initial state for our useReducer hook by defining  reducer function:

Consider the following JSX snippet for reducer:

import React from 'react'; const initialTodo = [...]; const todoReducer = (state, action) => {  switch (action.type) { case 'DO_TODO':      return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: true }; } else { return todo; } }); case 'UNDO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: false }; } else { return todo; } }); default: return state; } }; const App = () => { const [todos, dispatch] = React.useReducer( todoReducer, initialTodo ); const handleChange = () => {}; return ( <ul> {todos.map(todo => ( <li key={todo.id}> ... </li> ))} </ul> ); }; export default App;

Now we can use the handler to dispatch an action for our reducer function.  

Because we need the id as the identifier of a todo item in order to toggle it,  we can pass the item within the handler function by using a encapsulating arrow function as below:

const handleChange = todo => { dispatch({ type: 'DO_TODO', id: todo.id }); };

 And input will look like:

<input type="checkbox" checked={todo.complete} onChange={() => handleChange(todo)} />

Let's now implement to check our handler whether a todo item is completed or not by the below condition:

const handleChange = todo => { dispatch({ type: todo.complete ? 'UNDO_TODO' : 'DO_TODO', id: todo.id, }); };

 Putting up all this together, our component looks like:

import React from 'react'; const initialTodo = [ { id: 'a', task: 'Do Something', complete: false,   }, { id: 'b', task: 'Walk over here', complete: false, }, ]; const todoReducer = (state, action) => { switch (action.type) { case 'DO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: true }; } else { return todo; } }); case 'UNDO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: false }; } else { return todo; } }); default: return state; } }; const App = () => { const [todos, dispatch] = React.useReducer( todoReducer, initialTodo ); const handleChange = todo => { dispatch({ type: todo.complete ? 'UNDO_TODO' : 'DO_TODO', id: todo.id, }); }; return ( <ul> {todos.map(todo => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.complete} onChange={() => handleChange(todo)} /> {todo.task} </label> </li> ))} </ul> ); }; export default App;

 And here is the Output:

Now the biggest question is:

When do we use React.useReducer instead of React.useState? The answer is very simple and I will try to keep it even more simple:

We can Use React.useReducer when -----

  • Your application architecture is complex and big in size  
  • When the logic to update state is super complex or you want to update state deep down in your component tree
  • The state value is either an object or an array
  • You need a more predictable, and maintainable state architecture of the application

And, we can Use React.useState when -----

  • Your application is small
  • The state value is a primitive value
  • Simple UI state transitions
  • Logic is not complicated and can stay within one component

Conclusion

The useReducer hook is a nice addition to the React library which allows for a  simpler, predictable and organized way to update our component’s state and makes sharing data between components a bit easier.

It lets you optimize the performance of the components that trigger deep updates because now you can easily pass dispatch down instead of typical callbacks.

And even if you take nothing else from this article, you should at least remember this: that useReducer allows us to define how we update our state value.

Happy coding!

Profile

KnowledgeHut

Author
KnowledgeHut is an outcome-focused global ed-tech company. We help organizations and professionals unlock excellence through skills development. We offer training solutions under the people and process, data science, full-stack development, cybersecurity, future technologies and digital transformation verticals.