React Reducers

Chapter 28 29 mins

Learning outcomes:

  1. What are reducers
  2. Reducer in React specifically
  3. The useReducer() hook
  4. An example of useReducer()
  5. When and why to use useReducer()
  6. The ternary variant of useReducer()

Introduction

When we're creating complex apps in React, the notion of state is completely unavoidable. And why should it be. We just can't produce even a basic, interactive component in React without the least usage of state. State is an integral part of React, sitting right at its very core.

Now as we already know, working with state in modern-day React typically resolves to working with the useState() hook.

And as you'd agree, useState() is fairly simple to use. There isn't anything complex about it. We can use this hook to create as many state values inside a component as we want to and then work with them separately.

However, as separate state values keep on being added to a component, managing them all effectively becomes exponentially difficult. When there are a multitude of interactions in a component and each one updates multiple state values, it becomes a mess to keep on using useState() and addressing each of these state values separately.

A hefty amount of state logic in a component makes the component grow in size and then it's all as the saying goes: "Where there's function size, there's complexity." (Alright, alright...yes we made that up right now.)

Anyways, in this chapter, we shall learn about a beautiful, simple, and elegant way in React to solve this problem — using reducers.

In particular, we shall look into the useReducer() hook, its syntax and purpose, what is a reducer function, the notion of actions (with action types and data), and much more on this road. We shall even discover advanced usage of useReducer() in order to keep from running an expensive state initializing function.

There's a lot to learn in this chapter, so let's get started in 3, 2, 1, action!

What are reducers?

We'll begin with understanding what exactly is a reducer, then consider one simple, yet practical example of a reducer, and then finally learn about the useReducer() hook in React.

The concept of reducers isn't new in React; it existed long before in functional programming.

It's interesting to appreciate the reason for choosing the name 'reduce' — traditionally, programming languages have had a reduce() function/method to iterate over a sequence and reduce it down to one single value.

Remember the array reduce() method in JavaScript?

In this regard, a callback function would be provided to reduce() to define how a given element of the sequence interacts with the so-far-accumulated value.

And thus this callback function came to be known as a 'reducer function', or simply a 'reducer'.

Let's consider an example of using the reduce() array method in JavaScript — the classic example of computing the sum of a list of numbers using reduce().

Take a look at the following code:

var nums = [1, 2, 3, 4, 5];

We have a list of numbers and want to compute their sum. Not a difficult task is it? Just create a loop and add everything therein. Simple.

But what if we want nums's reduce() method to do the job instead? Alright, no problem even then:

var nums = [1, 2, 3, 4, 5];
var sum = nums.reduce((sum, num) => sum + num, 0);

The value returned by reduce() here represents the sum of the numbers, hence we initialize it to 0 via the second argument.

In each iteration over nums, internally done by reduce(), we get this accumulated sum and the current number of nums provided to the callback function. This callback adds the sum to the number and returns the result.

Returning has the effect of replacing the internally managed accumulator with the given value. Thus, in our case, at the end of each iteration over nums, the current sum gets replaced with the new sum.

And that's how simple it is to sum a list using reduce().

The function provided here to reduce() is the champion to cheer about. It's a reducer function.

Generally speaking, and taking into account how they are typically constructed in React, reducers have the following generic form:

function reducer(main, value) {
   // `value` is processed and then
   // a new `main` is returned here
}

The first parameter represents the main concerning value while the second parameter represents something that is bound to act upon this main value.

For instance, in our previous example, sum, i.e. the main value, represents the total sum of the numbers (which is what we were concerned with). num on the other hand simply represents an individual number that ought to be added to this sum.

This is essentially everything about a reducer. Now, let's find out how reducers look and work in React specifically.

It's worth mentioning here that when we're using a reducer in React, precisely there's nothing being 'reduced', so to speak; it's just that the term 'reducer' has become quite popular to placehold the respective concept from functional programming.

Reducers in React specifically

We're already familiar with a state object in React, containing all the individual state values as properties of that one single object, representing the entire state of the containing component.

In React, reducers typically operate on these state objects (although there's no necessity of doing so).

Following the general form above, reducers in React also have their first parameter representing the main value while the second parameter representing something to act upon the main value.

But it helps to see the general form of reducers specifically in React.

This is given as follows:

function reducer(state, action) {
   // `action` is processed and then
   // a new `state` is returned here
}

The first parameter represents the state while the second one represents an action that defines an interaction to update the state (for e.g. click, a focus, a timer update, etc.)

As you'll soon see, the most elegant part of a reducer in React is perhaps this action parameter; it can be used to provide additional details to a reducer on how to update the state (the main value) provided to it.

So what is an action?

An action is precisely the analogue of an event in an application — it literally represents some important 'action' in the app to which the app should respond (by updating its state).

Let's quickly consider an example of such a reducer and then build further upon it.

Consider the following string str:

var str = 'Using reducers';

Suppose we want to convert this string into uppercase characters using an action paired with a reducer that knows how to act upon this action.

Let's say that the action is modeled using a string such as 'UPPERCASE', and that the reducer function is called transform().

The idea is simple: when transform() is given 'UPPERCASE' as an action, it returns the first argument, i.e. the string to uppercase, in uppercase characters.

function transform(str, action) {
   switch (action) {
      case 'UPPERCASE':
         return str.toUpperCase();
   }
}

var str = 'Using reducers';
var newStr = transform(str, 'UPPERCASE');
console.log(newStr);

As this code demonstrates, another convention of reducer functions in React is that they use switch in order to determine the path to take depending on the given action.

To learn more about switch and about the surprising fall-through behavior, consider going through JavaScript Conditions — switch Statement.

Anyways, let's see what the console shows:

USING REDUCERS

Great.

Now, let's say we want to concatenate another string to str and get back the final, concatenated string. How could we accomplish this?

Well, this time, because we need to provide additional data to the reducer — the string to concatenate to str — we need a structure holding both the action's string and the data.

Trivially enough, we just seek an object literal for this:

{
   type: 'CONCAT',
   data: ' concatenate me!'
}

The type property, another convention in reducers in React, specifies the nature of the action while data specifies its additional data.

There are many possible ways to name this additional data field: data, payload, body, value. Choose whatever works best for you and your reducer.

Here's an important note on setting up action type names:

Deciding action type

The action types are determined by us when designing the component and the reducer. Since the action is...well...an action, we must make sure to name these types after a verb.

So for example, the action type 'INCREMENT' is much better than 'INCREMENTED' or 'INCREMENTING'. Similarly, 'REGISTER' is better than 'REGISTRATION'. And so on.

Following we augment our transform() reducer to enlist this concatenation action:

function transform(str, action) {
   switch (action.type) {
      case 'UPPERCASE':
         return str.toUpperCase();
case 'CONCAT':
return str + action.data; } }

Let's now try concatenating two strings using transform() and this newly-added action:

function transform(str, action) { /* ... */ }

var str = 'Using reducers';
var newStr = transform(str, { type: 'CONCAT', data: ' concatenate me!' });
console.log(newStr);
Using reducers concatenate me!

Perfect.

So now that we know what a reducer exactly is and how reducers are typically created in React, the next step is to actually use them in managing the state of our components.

It's time to look into useReducer().

The useReducer() hook

The useReducer() hook in React provides an extremely powerful state management utility at our disposal to use in our components.

At its core:

useReducer() is used to set a state value on a component and configure a reducer to be called upon the occurrence of a given event/action in order to update the state.

Following is the syntax of useReducer():

useReducer(reducer, initialValue)

The first reducer argument specifies the reducer function, as we discussed in the previous section. The second argument initialValue specifies the initial state value in the case of two arguments.

In the case of three arguments, however, the second argument specifies a value to provide to the third initialCallback argument which is a callback function used to create the initial state value:

useReducer(reducer, initialArg, initialCallback)

We'll see a use case of this ternary variant of useReducer() later on in this chapter.

The return value of useReducer() is quite similar to that of useState(). That is, useReducer() returns an array consisting of two elements:

  1. The first one is the current state value.
  2. The second one is a function to update the state.

Most importantly though, this function returned by a useReducer() call does NOT work the same as the state updater function returned by a useState() call.

In particular, the second item of the array returned by useReducer() is a function, commonly referred to as a dispatcher, that tells React that an action in the app has been dispatched.

This results in React automatically calling the configured reducer with the latest state value and the dispatched action, i.e. the argument provided to the dispatcher at invocation. The reducer processes the action and works it into generating a new state. This state is then ultimately used to update the current state value in the component.

Alright, it's time to help steer clear of all the cloudiness in all this discussion of useReducer(). It's time to consider an example

A simple example

We'll consider one of the simplest of programs to demonstrate state management via reducers in React — a counter.

Suppose we have a counter program with a display of the current count and a set of buttons to increment, decrement and reset it. All pretty elementary stuff.

A simple counter program made in React
A simple counter program made in React

Here's the code of the program, written using useState():

import { useState } from 'react';

function Counter() {
   const [count, setCount] = useState(0);
   return (
      <div>
         <h1>{count}</h1>
         <button onClick={() => setCount(0)}>Reset</button>
         <button onClick={() => setCount(count - 1)}>Decrement (-)</button>
         <button onClick={() => setCount(count + 1)}>Increment (+)</button>
      </div>
   )
}

Evidently, there's nothing special to address here; it's all just fundamental React.

Let's now say we want to convert this same program from interfacing with the state directly to interfacing with it via a reducer.

Here's how we'll begin:

Since we know that a reducer acts upon a state value via an action, we have to think about the representation of the state and the representation of this action.

Should the state be an object containing individual state-related properties or just a primitive value. Should the action be a string or an object with a type property holding the type of the action?

Well, in this case, it's not that difficult to reason about this:

  • The state can just be the count (instead of an object with a count property).
  • The action can just be a string, such as 'INCREMENT' (because at least now we don't need any additional data).

Perfect. With the representations of the state and the action decided, next up we construct the reducer.

Let's call it getNewCount(), as it's tasked with taking in the state of the counter along with a fired action and then returning back the new state accordingly:

function getNewCount(count, action) {
   switch (action) {
      case 'RESET':
         return 0;
      case 'DECREMENT':
         return count - 1;
      case 'INCREMENT':
         return count + 1;
   }
}

As per the counter program shown above, there are three possible actions that can be performed on a counter: a reset, given by 'RESET'; a decrement, given by 'DECREMENT'; and an increment, given by 'INCREMENT'.

After coding the reducer, in the last step we just need to set up state management using it inside the concerned component, via useReducer().

Here's this step for our counter program:

import { useReducer } from 'react';

function getNewCount(count, action) { /* ... */ }

function Counter() {
   const [count, dispatch] = useReducer(getNewCount, 0);
   return (
      <div>
         <h1>{count}</h1>
         <button onClick={() => dispatch('RESET')}>Reset</button>
         <button onClick={() => dispatch('DECREMENT')}>Decrement (-)</button>
         <button onClick={() => dispatch('INCREMENT')}>Increment (+)</button>
      </div>
   )
}

The initial state value is 0 since the counter begins at a count of 0. Besides this, the buttons dispatch either of the actions, 'RESET', 'DECREMENT', or 'INCREMENT', each of which then results in the getNewCount() reducer being called to update the count state.

Check out this example live below:

Live Example

If we wish not to decrement the counter when it reaches a count of 0, we don't need to even look into Counter; only getNewCount() is where we have to go.

Consider the following rewrite of getNewCount() to make sure that when the count is 0, no decrement is made.

function getNewCount(count, action) {
   switch (action) {
      case 'RESET':
         return 0;
      case 'DECREMENT':
         return (count === 0) ? count : count - 1;
      case 'INCREMENT':
         return count + 1;
   }
}

The ternary operator in JavaScript is used to determine whether to return count as it is or count - 1.

Live Example

All in all, using useReducer(), we get the same counter program that we demonstrated above using useState() but obviously in terms of behavior only; the code and the approach to powering that behavior is definitely not the same.

When and why to use useReducer()

When building a component in React, if we find ourselves wrestling with updating a multitude of state values, it's the high time to consider transitioning to managing state using a reducer.

With a reducer, we dispatch an action (remember the dispatch() function returned by useReducer()) and provide additional data along with it for the reducer to figure out what new state to precisely return.

We simply let go off our worry about manually updating individual state values — the reducer takes care of everything.

All in all, there are quite a few benefits of managing state via reducers and useReducer().

Here are they:

  • Component code gets simplified and made less complex
  • State update logic becomes more declarative and less imperative
  • A reducer function is defined separately and can be tested on its own
  • It's easier to share multiple state values of a parent component with multiple child components

While creating a reducer and then managing state using it and the useReducer() hook ain't any difficult, it's NOT recommended to start off with this approach.

In other words, you should always begin with naive state management in your components powered by useState().

It should only be when managing useState() becomes quite strenuous that you bring in a reducer and useReducer() to deal with state management in a better way.

Emphasizing on it once more, never ever begin with a reducer. Always start with the basic useState() hook and then, as you develop your component, see whether you really need the ultimate power of reducers to manage state.

In fact, this is an integral part of creating React apps, going well beyond just managing state. Start off with the basic principles and only add in extra features (such as memoization, reducers) when the need really be.

The ternary variant of useReducer()

Often times, when initializing state via useReducer(), we'd have the concerned value right with us, to be passed at it is to the hook.

However, on some rare occasions, we might want to first process the data using a custom function and intialize state only based on the final result:

For example, let's suppose we have a very complex Autocompleter component that renders an input field along with a suggestions box.

function Autocompleter({ data: initialData }) {
   const [data, dispatch] = useReducer(dataReducer, initialData);
   ...
   return (
      <>
         <AutocompleteInput ... />
         <SuggestionsBox ... />
      </>
   );
}

As the user types into the field, suggestions are shown for the entered query based on given set of data, provided when instantiating the component.

Now, the data prop provided to this component, which is used to initialize the data state, might be one of different kinds of structures: objects, arrays, maybe even sets, etc.

This data prop needs to be normalized into a single proprietary data structure that Autocompleter and its child components are designed to understand and work with.

Let's say that we already have a function getNormalizedData() that takes of this. We just need to call it in our component.

In this respect, we might go on and do the following:

function Autocompleter({ data: initialData }) {
   const [data, dispatch] = useReducer(dataReducer, getNormalizedData(initialData));
   ...
   return (
      <>
         <AutocompleteInput ... />
         <SuggestionsBox ... />
      </>
   );
}

We're calling getNormalizedData() directly inside Autocompleter, providing its return value to the second argument of useReducer() to initialize the underlying state.

This is a senseless thing.

As we already know, React doesn't concern itself with the initial value provided to useReducer() (and even to useState()) after the concerning component renders for the very first time (i.e. when it mounts).

This means that in the code above, each time Autocompleter gets re-rendered, getNormalizedData() data is called, yet its return value is completely ignored by React.

So does calling getNormalizedData() really make any sense? Certainly no.

And if we also assume that getNormalizedData() is quite an expensive function, then this fact becomes even more important — that is, we should only call it once.

In order to allow for such a use of useReducer(), the hook has a ternary variant, accepting three arguments:

initialArg is the initial argument to provide to the callback function. In this variant, useReducer() calls callback(initialArg) itself and that only on the first occasion the component renders.

Hence, the code above should be rewritten as follows:

function Autocompleter({ data: initialData }) {
   const [data, dispatch] = useReducer(dataReducer, initialData, getNormalizedData);
   ...
   return (
      <>
         <AutocompleteInput ... />
         <SuggestionsBox ... />
      </>
   );
}

The beauty of this is that we don't need to change anything in the reducer function or make any other changes anywhere in the underlying component.

We have two simple things: don't call the function; and do precede it with an argument which is to be provided to it upon invocation.