DEV Community

Cover image for A Complete Beginner's Guide to useReducer Hook
Mihaela for WorksHub

Posted on • Originally published at works-hub.com

A Complete Beginner's Guide to useReducer Hook

Introduction

useReducer is a React Hook introduced late in October 2018, which allows us to handle complex state logic and action. It was inspired by Redux state management pattern and hence behaves in a somewhat similar way.

Oh! but don't we already have a useState hook to handle state management in React?

Well, yes! useState does the job pretty well.
However,
the useState hook is limited in cases where a component needs a complex state structure and proper sync with the tree. useReducer when combined with useContext hook could behave very similarly to Redux pattern and sometimes might be a better approach for global state management instead of other unofficial libraries such as Redux.
As a matter of fact, the useReducer's API itself was used to create a simpler useState hook for state management.

According to React's official docs :

"An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method."

A call to Reduce Method in JavaScript



To begin with useReducer, first, we need to understand how JavaScript's built-in Array method called Reduce works, which shares remarkable similarity with the useReducer hook.

The reduce method calls a function (a reducer function), operates on each element of an array and always returns a single value.

function reducer(accumulator, currentvalue, currentIndex, sourceArray){
  // returns a single value
}

arrayname.reduce(reducer)
Enter fullscreen mode Exit fullscreen mode

As stated, the above reducer function takes in 4 parameters -

1. Accumulator : It stores the callback return values.

2. Current Value : The current value in the array being processed.

3. Current Index (optional) : The index of the current value in the array being processed.

4. Source Array : The source of the array on which reduce method was called upon.

Let's see reduce function in action, by creating a simple array of elements :

const items = [1, 10, 13, 24, 5]
Enter fullscreen mode Exit fullscreen mode

Now, We will create a simple function called sum, to add up all the elements in the items array. The sum function is our reducer function, as explained above in the syntax

const items = [1, 10, 13, 24, 5]

function sum(a,b, c, d){
    return a + b
}

Enter fullscreen mode Exit fullscreen mode

As we can see, I am passing four parameters named a, b, c, d, these parameters can be thought of as Accumulator, Current Value, Current Index, Source Array respectively.

Finally, calling the reduce method on our sum reducer function as follows

const items = [1, 10, 13, 24, 5];

function sum(a, b, c, d){
    return a + b;
}

const out = items.reduce(sum);

console.log(out);

OUTPUT :
59

Enter fullscreen mode Exit fullscreen mode

Let's understand what's going on here :

When the reduce method gets called on the sum function, the accumulator ( here a) is placed on the zeroth index (1), the Current Value (here b) is on 10. On the next loop of the sum function, the accumulator adds up a + b from the previous iteration and stores it up in accumulator (a) while the current value(b) points to 13 now.
Similarly, the accumulator keeps on adding the items from the array whatever the Current Index is pointing until it reaches the very end of it. Hence resulting in the summation of all the items in the array.

// We can do a console.log to check iteration in every function loop :

const items = [1,10,13,24,5];

function sum(a, b, c, d){
   console.log("Accumulator", a)
   console.log("Current Index", b)
     return a + b
}

const out = items.reduce(sum);

console.log(out);

'Accumulator' 1
'Current Index' 10
'Accumulator' 11
'Current Index' 13
'Accumulator' 24
'Current Index' 24
'Accumulator' 48
'Current Index' 5
53


Enter fullscreen mode Exit fullscreen mode

In addition to this, there is an optional initial value, when provided will set the accumulator to the initial value first, before going out for the first index item in the array. The syntax could look like this :

items.reduce(sum, initial value)
Enter fullscreen mode Exit fullscreen mode

While we just finished understanding how the reduce method works in JavaScript, turns out both the Redux library and the useReducer hook shares a common pattern, hence the similar name.

reduce v_s useReducer.png

How does useReducer works?



useReducer expects two parameters namely, a reducer function and an initial state.

useReducer(reducer, initialState)
Enter fullscreen mode Exit fullscreen mode

Again the reducer function expects two parameters, a current state and an action and returns a new state.

function reducer(currentState, action){
    returns newState;
}
Enter fullscreen mode Exit fullscreen mode

useReducer Hook in Simple State and Action



Based on what we have learnt so far, let's create a very simple counter component with increment, decrement feature.

We will begin by generating a JSX component :

import React from 'react';
import ReactDOM from 'react';

function App(){
  return (
    <div>
        <button onClick={handleIncrement}>+</button>
        <span>{state}</span>
        <button onClick={handleDecrement}>-</button>
    </div>
  );
}
// define a root div in a HTML file and mount as such
ReactDOM.render(<App />, document.getElementById("root"));

Enter fullscreen mode Exit fullscreen mode

Create a reducer function, expecting a state and an action. Also, attach onClick events on both buttons and define the click functions within the App component :

import React, {useReducer} from 'react';
import ReactDOM from 'react';

function reducer(state, action){
  // return newState
}


function App(){
  function handleDecrement() {
    // ...
  }

  function handleIncrement() {
    // ...
  }

return (
  <div>
    <button onClick={handleIncrement}>+</button>
    <span>{state}</span>
    <button onClick={handleDecrement}>-</button>
  </div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

Moving onwards, before we trigger useReducer hook, it is important to note that it returns an array of two values,

const state = useReducer[0]
const dispatch = useReducer[1]
Enter fullscreen mode Exit fullscreen mode

We can simplify the above, using array destructuring (a best practice) :

const [state, dispatch] = useReducer(reducer, intialState)
Enter fullscreen mode Exit fullscreen mode

Now, coming back to our counter component and including the above useReducer snippet in it

function reducer(state, action){
  if (action === "increment") {
    return state + 1;
  } 
  else if (action === "decrement") {
    return state - 1;
  } 
  else null;
}


function App(){
  function handleDecrement() {
    dispatch("decrement");
  }

  function handleIncrement() {
    dispatch("increment");
  }

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

return (
  <div>
    <button onClick={handleIncrement}>+</button>
    <span>{state}</span>
    <button onClick={handleDecrement}>-</button>
  </div>
);
}


Enter fullscreen mode Exit fullscreen mode

Link to codepen

The handleIncrement and handleDecrement function returns a dispatch method with a string called increment and decrement respectively. Based on that dispatch method, there is an if-else statement in the reducer function which returns a new state and eventually triggering (overriding in a way) the state in useReducer.

According to the official docs, always use Switch statements in the reducer function (you already know this if you have worked with Redux before), for more cleaner and maintainable code. Adding more to this, it is advisable to create an initial state object and pass a reference to the useReducer

const initialState = { 
  count: 0 
  // initialize other data here
}
const [state, dispatch] = React.useReducer(reducer, intialState);

Enter fullscreen mode Exit fullscreen mode

useReducer Hook in Complex State and Action



Let's see the same counter component, building it with what we have learnt so far but this time with a little complexity, more abstraction, also best practices.

const initialState = {
  count: 0
};

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.value };
    case "decrement":
      return { count: state.count - action.value };
  }
}

function App() {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  return (
    <div>
      <button onClick={() => dispatch({ type: "increment", value: 5 })}>
        +
      </button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: "decrement", value: 5 })}>
        -
      </button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Link to codepen

What has changed?

  1. Instead of passing a value directly to the useReducer hook, we have an object initialised with a count property set to zero. This helps in cases when there are more than a single property to be initialised also easier to operate on an object.

  2. As we discussed earlier, if-else has been modified to switch based statements in the reducer function.

  3. The dispatch method is now object-based and provides two properties type and value. Since the dispatch method triggers action, we can switch statements in the reducer function using action.type. Also, the new state can be modified by using a dynamic value which can be accessed on action.value

Dealing with Multiple useReducer Hook



When dealing with multiple state variables that have the same state transition, sometimes it could be useful to use multiple useReducer hook that uses the same reducer function.

Let's see an example :

const initialState = {
  count : 0
}

function reducer(state, action) {
switch (action) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
      default : 
      return state
  }
}

function App() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const [stateTwo, dispatchTwo] = React.useReducer(reducer, initialState);

return (
  <>
    <div>
        <button onClick={() => dispatch('increment')}>+</button> 
        <span>{state.count}</span>
        <button onClick={() => dispatch('decrement')}>-</button>
    </div>
    <div>
        <button onClick={() => dispatchTwo('increment')}>+</button>
        <span>{stateTwo.count}</span>
        <button onClick={() => dispatchTwo('decrement')}>-</button>
    </div>
  </>
);
}



ReactDOM.render(<App />, document.getElementById("root"));



Enter fullscreen mode Exit fullscreen mode

Here we are using two useReducer hook with different dispatch and state triggering the same reducer function.

Link to codepen

useState v/s useReducer

Here is a quick rundown, comparing both the hooks :

useState v_s useReducer.png

useReducer's Rendering Behavior

React renders and re-renders any useReducer component very similar to the useState hook.

consider the following contrived example where + increments the count by 1, - decrements the count by 1 and Reset restores the count value to 0.

function App(){
  const [count, dispatch] = useReducer(reducer, initialState)
  console.log("COMPONENT RENDERING");
    return (
      <div>
          <div>{count}</div>
          <button onClick={() => {dispatch('increment')}>+</button>
          <button onClick={() => {dispatch('decrement')}>-</button>
          <button onClick={() => dispatch('reset')}>Reset</button>  
      </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

The above App component :

1. Re-render every time the count state changes its value, hence logging out the COMPONENT RENDERING line.

2. Once, the reset button has been clicked, the subsequent clicks to the reset button won't re-render the App component as the state value is always zero.





While we just finished reading how rendering happens in the context of useReducer, we have reached the end of this article!

Some Important Resources that I have collected over time:

1. https://reactjs.org/docs/hooks-reference.html#usereducer

2. https://geekflare.com/react-rendering/

3. https://kentcdodds.com/blog/should-i-usestate-or-usereducer

4. https://kentcdodds.com/blog/application-state-management-with-react

Loved this post? Have a suggestion or just want to say hi? Reach out to me on Twitter

Originally written by Abhinav Anshul for JavaScript Works

Discussion (0)