This page looks best with JavaScript enabled

How to fight bad state with useReducer

 ·  ☕ 5 min read  ·  ✍️ Iskander Samatov

usereducer-prevents-bad-state


Intro

In this post, I’ll cover a quick tip in React state management that came to my mind recently. It’s a step toward preventing the introduction of a bad or unexpected state in your React application as it grows in complexity.

What is a bad state, and how does it happen?

So what is a bad state? A bad state is an edge case that your application doesn’t account for and doesn’t know how to handle. When your application ends up in a bad state, it results in a broken UX for your users, making your app seem janky and unpolished.

You end up with a bad state when a component keeps growing in complexity, and more things need to be accounted for to keep it working properly.

It won’t happen immediately but over time. As you add logic to your state, certain edge cases invariably slip through the cracks.

Let’s take a look at an example. Say you have a user profile dashboard. It needs to do three basic things: fetch user information, display the loader while fetching, and show an error banner if there’s an API error.

So here’s what the code for the component might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useEffect } from 'react';
import { fetchUserInfo } from './api';

export const UserDashboard = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)


  useEffect(() => {
    fetchUserInfo().then(response => {
      setUser(response)
      setLoading(false)
    }).catch((e) => {
      setError(e.message)
    });
  }, []);


  return <div>
    {loading && <p>Loading</p>}
    {error && <p>Error fetching response</p>}
    {!loading && <p>{user.name}</p>}
  </div>
};

Can you spot the problem? If there’s an API error, our app will keep displaying the loader together with the error banner. It is not a huge problem and can be easily fixed. But it illustrates our point - bugs like these are not immediately obvious.

When you have multiple state variables that are being mutated all over the place, you make your component harder to understand, and you will slowly start introducing bad state edge cases that are tougher to spot.

This happens when you keep adding useState variables to your component as it grows without refactoring its state.

Use useReducer to fix a bad state

So, how can we prevent this from happening? Unfortunately, there isn’t a magic-wand solution to prevent bugs like these altogether . But there are some small steps we can take to improve our odds and the readability of our code.

One such step is to switch to using useReducer instead of useState when your component state grows.

In the example of our UserDashboard let’s rewrite it to use the useReducer hook instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { useEffect, useReducer } from "react";
import { fetchUserInfo } from "./api";

const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_ERROR = "FETCH_ERROR";

const DEFAULT_STATE = {
  user: null,
  loading: false,
  error: null
};

const reducer = (state, { type, payload }) => {
  switch (type) {
    case FETCH_SUCCESS: {
      return { ...state, user: payload.user, loading: false, error: null };
    }
    case FETCH_ERROR: {
      return { ...state, user: null, error: payload.error, loading: false };
    }
    default:
      return state;
  }
};

export const UserDashboard = () => {
  const [state, dispatchState] = useReducer(reducer, ...DEFAULT_STATE);
  const { loading, error, user } = state;

  useEffect(() => {
    fetchUserInfo()
      .then((response) => {
        dispatchState({ type: FETCH_SUCCESS, payload: { user: response } });
      })
      .catch((e) => {
        dispatchState({ type: FETCH_ERROR, payload: { error: e.message } });
      });
  }, []);

  return (
    <div>
      {loading && <p>Loading</p>}
      {error && <p>Error fetching response</p>}
      {!loading && <p>{user.name}</p>}
    </div>
  );
};

Now, we’re using a reducer to mutate the component’s state. You should be familiar with reducers if you’ve worked with Redux before. If you want to learn more about useReducer hook and reducers, in general, check out this React documentation page .

So what exactly are we gaining by using useReducer instead of useState? A couple of things, let’s cover them.

Having all of the state mutations in one place simplifies debugging it. And it makes it easier to spot instances where you accidentally introduced a bad state. If we didn’t do it, we would have state mutations sprinkled all over the component instead. With the state reducer pattern, we’re taking one step towards clarity by having a single source of truth for all of our state mutations.

There’s also a slight performance benefit to our new approach. Previously, we would call state updates multiple times. We would separately update our state variables for loading, error, and user data using separate instances of the useState hook. Now we have a switch statement, with each of its cases taking care of all the necessary variables at once and returning a new state.

Conclusion

So there you have it - a quick tip or, for some, a simple reminder to use useReducer instead of useState as the complexity of your component grows to avoid introducing bad or unexpected states.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.