How to use useReducer in React Hooks for performance optimization

Understanding 6 different useReducer use cases for React Hooks API

Daishi Kato
Feb 13 · 8 min read

Introduction

React Hooks API is officially released in React 16.8.In this post, we focus especially on useReducer by introducing various use cases. Before continuing reading this tutorial, please read the official document if you haven’t. This tutorial assumes readers already have a basic understanding of hooks.

(Also, try out the Crowdbotics App Builder to instantly scaffold and deploy a React application.)

The useReducer hook is listed in Additional Hooks.

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

Although useState is a Basic Hook and useReducer is an Additional Hook, useState is actually implemented with useReducer. This means useReducer is primitive and you can use useReducer for everything you can do with useState. Reducer is so powerful that it can apply for various use cases.

The rest of this tutorial consists of various examples. Each example shows a certain use case and we show working code.

Example01: Minimal pattern

Let’s look at the simplest example code. We mostly use the counter example throughout this tutorial.

const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};

We first define an initialState and a reducer. Note that the state here is a number, not an object. Redux users might get confused, but this is just fine. Furthermore, the action is a plain string here.

The following is a component with useReducer.

const Example01 = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};

When a user clicks a button, it will dispatch an action which updates the count and the updated count will be displayed. You could define as many actions as possible in the reducer, but the limitation of this pattern is that actions are finite.

The full working code can be found below:

Example02: Action object

This example is the one that is familiar to Redux users. We use a state object and an action object.

const initialState = {
count1: 0,
count2: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case 'increment1':
return { ...state, count1: state.count1 + 1 };
case 'decrement1':
return { ...state, count1: state.count1 - 1 };
case 'set1':
return { ...state, count1: action.count };
case 'increment2':
return { ...state, count2: state.count2 + 1 };
case 'decrement2':
return { ...state, count2: state.count2 - 1 };
case 'set2':
return { ...state, count2: action.count };
default:
throw new Error('Unexpected action');
}
};

In this example, we keep two numbers in a state. We could use a complex object for a state as long as we organize a reducer well (ref: combineReducers). Because the action in this example is an object, we can put values like action.count in addition to a type. The reducer in this example is a bit of mess, but this allows us to simplify the component as the following.

const Example02 = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<div>
{state.count1}
<button onClick={() => dispatch({ type: 'increment1' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>
<button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>
</div>
<div>
{state.count2}
<button onClick={() => dispatch({ type: 'increment2' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>
<button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>
</div>
</>
);
};

Notice there are two counters in a state, and action types are defined to update one counter out of the two.

See the full working code below:

Example03: Multiple useReducers

The previous example has two counters with a single state, which is a typical approach for global state. Because we are only working with local state, there is another way. We can use useReducer twice. Let’s look at the reducer.

const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'set': return action.count;
default: throw new Error('Unexpected action');
}
};

The state here is a simple number instead of an object, which is the same in Example01. Note that the action here is an object, which is different from that in Example01.

The component using this reducer will be the following.

const Example03 = () => {
const [count1, dispatch1] = useReducer(reducer, initialState);
const [count2, dispatch2] = useReducer(reducer, initialState);
return (
<>
<div>
{count1}
<button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
</div>
<div>
{count2}
<button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
</div>
</>
);
};

As you can see, we have two dispatch functions for each counter. We share the same reducer function for both.

The functionality is identical to that of Example02. The full working code is below.

Example04: TextInput

Let’s look at a realistic example in which multiple useReducers work well. Suppose we have a React Native-like TextInput component, and we want to store text in local state. We can use a dispatch function to update the text.

const initialState = '';
const reducer = (state, action) => action;

Note that the old state is just thrown away each time the reducer is called. The component using this is the following.

const Example04 = () => {
const [firstName, changeFirstName] = useReducer(reducer, initialState);
const [lastName, changeLastName] = useReducer(reducer, initialState);
return (
<>
<div>
First Name:
<TextInput value={firstName} onChangeText={changeFirstName} />
</div>
<div>
Last Name:
<TextInput value={lastName} onChangeText={changeLastName} />
</div>
</>
);
};

How simple it is. You could add some validation logic in reducer too. See the full example code below.

Example05: Context

At some point, we might want to share state between components a.k.a global state. In general, global state tends to limit component reusability, hence first consider using local state and only passing them (incl. dispatch) by props. When it doesn’t work well, Context is a rescue. If you are not familiar with Context API, check out the official document and how to use useContext.

In this example, we use the same reducer in Example03. The following is the code on how to create a context.

const CountContext = React.createContext();

const CountProvider = ({ children }) => {
const contextValue = useReducer(reducer, initialState);
return (
<CountContext.Provider value={contextValue}>
{children}
</CountContext.Provider>
);
};

const useCount = () => {
const contextValue = useContext(CountContext);
return contextValue;
};

The function useCount is called custom hooks which can be used just like normal hooks. For more information about custom hooks, please read the official document.

The component code is the following with useCount.

const Counter = () => {
const [count, dispatch] = useCount();
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);
};

As our contextValue is just the result of useReducer, we destructure the result of useCount in the same way. Note that, at this point, it’s uncertain which context is used.

Finally, here’s the code to use it.

const Example05 = () => (
<>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
</>
);

We have two CountProviders here. It means there are two counters, even though we have only one context. The Counters inside the same CountProvider shares the state. You might need to learn how this works by running the example code and trying it.

The full working code is below.

Example06: Subscription

Context is the preferred way to share state among components, but what if we already have a shared state outside of React components. We can technically subscribe to such a shared state and update components when the shared state is updated. This pattern has limitations and React team provides a utility package: create-subscription.

Unfortunately, the utility package is not yet for React Hooks as of writing, so we do our best with hooks for now. Let’s try to reproduce the same functionality of Example05 without Context.

First, here’s a tiny custom hook to be used.

const useForceUpdate = () => useReducer(state => !state, false)[1];

This reducer is simply to invert the previous state, ignoring the action. [1] is to return dispatch without destructuring. Next up is the main function to create a shared state and returns a custom hook.

const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach(callback => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
callback(); // in case it's already updated
const cleanup = () => {
const index = subscribers.indexOf(callback);
subscribers.splice(index, 1);
};
return cleanup;
}, []);
return [state, dispatch];
};
return useSharedState;
};

We use a new useEffect hook. It’s a very important hook, and you should carefully read the official document to learn how it works. In useEffect, we subscribe a callback to force update the component. We also clean up the subscription when the component is unmounted.

Let us create two shared states. We use the same reducer and initialState in Example05 and Example03.

const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);

Unlike useCount in Example05, these hooks are tied to specific shared states. We then use these two hooks.

const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);

const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />
};

const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />
};

Notice the Counter component is a stateless component in common. Finally, we use these components.

const Example06 = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);

By comparing this with the code in Example05, you should notice the difference. Instead of having context providers, our components and hooks in this example are already bound to shared states. The result is identical to Example05 (at least for now).

Demos

All the examples above can be accessible in StackBlitz. You can run, fork and edit as you like. Click the button below to start.

Final notes

This tutorial mainly describes useReducer with examples. Some other hooks are introduced in the examples, but intentionally useMemo (&useCallback) is left out. This hook is important for performance optimization and you should take time to learn it.

Disclaimer: The author did pay attention to correctness, but it may include some mistakes and/or bad practices.

Building A Web Or Mobile App?

Crowdbotics is the fastest way to build, launch and scale an application.

Developer? Try out the Crowdbotics App Builder to quickly scaffold and deploy apps with a variety of popular frameworks.

Busy or non-technical? Join hundreds of happy teams building software with Crowdbotics PMs and expert developers. Scope timeline and cost with Crowdbotics Managed App Development for free.

Crowdbotics

The fastest way to build your next app.

Thanks to Gaurav Agrawal

Daishi Kato

Written by

A freelance programmer. I’m interested in working remotely with people abroad: https://contact.axlight.com https://blog.axlight.com

Crowdbotics

The fastest way to build your next app.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade