Rui Fernandes

Simplifying State Management with useReducer on React.js apps

by Rui Fernandes -

Imagem de cover

Introduction

State management is an issue that we need to handle on user interfaces. In the last article, I showed how to manage states on React.js functional components with the useState hook.

However, in some complex use cases, useState can cause code repetition and complicate its maintainability. Because of that, in this article, we'll see how to simplify that complex cases with the useReducer hook.

Before reading this article

Make sure you have:

The problem

Let's suppose you will use a Feedback component that can show error, success, warning, and info messages. This component has the following properties:

There is its usage:

<FeedbackLabelComponent
  icon={ErrorIcon}
  color="#FF0000"
  message="Unexpected error!"
/>

In this example, we'll use the Feedback component in a Social Network page that has a button to publish text messages on the feed:

import React from 'react';
import {
  InitialIcon,
  ErrorIcon,
  SuccessIcon
} from 'some-icon-lib';
import { colors } from 'some-colors';
import { publishMessageService } from 'some-services';
import { FeedbackLabelComponent } from 'some-components';

export const PublishMessageScreen: React.FC = () => {
  /**
   * STATES 
   */
  const [icon, setIcon] = useState(InitialIcon);
  const [color, setColor] = useState(colors.black);
  const [feedbackMessage, setFeedbackMessage] = useState('');
  const [feedbackVisible, setFeedbackVisible] = useState(false);

  async function handlePublishMessage(message) {
    try {
      const response = await publishMessageService(message);
      /*
        Change state to success
      */
      setIcon(SuccessIcon);
      setColor(colors.success);
      setFeedbackMessage('Success!');
      setFeedbackVisible(true);
    } catch (error) {
      /*
        Change state to error
      */
      setIcon(ErrorIcon);
      setColor(colors.error);
      setFeedbackMessage('Error! ' + error.message);
      setFeedbackVisible(true);
    }
  }

  return (
    <div>
      <FeedbackLabelComponent
        icon={icon} 
        color={color}
        message={feedbackMessage}
        visible={feedbackVisible}
      />
      <button onClick={() => handlePublishMessage('Example message'))}>
        Publish message
      </button>
    </div>
  );
};

Notice that we needed to update four states to change the Feedback component properties.

This solution can look good but imagine a most complex use case that has multiple functions updating those states, for example. The state update would be repetitive and hard to manage.

How can we solve that problem? With the useReducer hook!

The useReducer hook

The useReducer hook simplifies state management. With this hook, we can only call a single function to change all states.

It has three parameters:

This is the useReducer call:

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

This hook returns a two-position array as the useState:

  1. state: current state
  2. dispatch: function to update state

Hands on

Initial state

First, we need to create (inside PublishMessageScreen) the initial state and its type:

// (...)
import { IconProps } from 'some-icon-lib';

// Type
export interface StateProps {
  icon: React.FC<IconProps>;
  color: string;
  message: string;
}

// Initial state
const initialState: StateProps = {
  icon: CheckIcon,
  color: '',
  message: ''
};

export const PublishMessageScreen: React.FC = () => {
  // (...)

Reducer

The reducer is a callback function that contains the state update logic. It receives as a parameter:

Ambos podem possuir tipagem criada pelo próprio programador, como é o nosso caso: Both can have types created by the developer as the following:

// (...)
export type stateMode = 'success' | 'warning' | 'error' | 'hidden';

export interface ActionProps {
  mode: stateMode;
}

function reducer(state: StateProps, action: ActionProps): StateProps {
  switch (action.mode) {
    case 'error':
      return {
        icon: XIcon,
        color: colors.red[500],
        message: 'Unexpected error!'
      }
    case 'success':
      return {
        icon: CheckIcon,
        color: colors.green[500],
        message: 'Success!'
      }
    case 'warning':
      return {
        icon: AlertIcon,
        color: colors.yellow[500],
        message: 'This is a warning'
      }
    case 'hidden':
      return {
        icon: CheckIcon,
        color: '',
        message: ''
      }
    default:
      throw new Error();
  }
}

export const PublishMessageScreen: React.FC = () => {
  // (...)

This function is called every time the user invokes dispatch.

Notice that it updates the state based on the action passed on dispatch.

The integration

After creating the initial state and the reducer, let's integrate everything, calling the useReducer hook inside the function component:

// (...)
 export const PublishMessageScreen: React.FC = () => {
  /**
   * STATE
   */
  const [feedbackState, dispatch] = useReducer(reducer, initialState);
  // (...)

Now let's call dispatch in the publish message function:

  async function handlePublishMessage(message) {
    try {
      const response = await publishMessageService(message);
      dispatch({ mode: 'success' })
    } catch (error) {
      dispatch({ mode: 'error' })
    }
  }

We should also update the feedback component properties we are passing:

  // (...)
  return (
    <div>
      <FeedbackLabelComponent
        {...feedbackState}
      />
      // (...)
    </div>
  );

Also, you can separate the types in a props.ts file and the reducer function in a method.ts file. The project folder structure will be like this:

(...)
node_modules
src
├── components 
│   ├── FeedbackLabelComponent
│   │   ├── index.tsx
├── pages
│   ├── PublishMessageScreen
│   │   ├── index.tsx
│   │   ├── props.ts
│   │   ├── methods.ts
(...)

We'll have the following result on the code:

import { IconProps } from 'some-icon-lib';

export type stateMode = 'success' | 'warning' | 'error' | 'hidden';

export interface ActionProps {
  mode: stateMode;
}

export interface StateProps {
  icon: React.FC<IconProps>;
  color: string;
  message: string;
}
import { ActionProps, StateProps } from "./props";
import {
  InitialIcon,
  ErrorIcon,
  SuccessIcon
} from 'some-icon-lib';
import { colors } from 'some-colors';

function reducer(state: StateProps, action: ActionProps): StateProps {
  switch (action.mode) {
    case 'error':
      return {
        icon: XIcon,
        color: colors.red[500],
        message: 'Unexpected error!'
      }
    case 'success':
      return {
        icon: CheckIcon,
        color: colors.green[500],
        message: 'Success!'
      }
    case 'warning':
      return {
        icon: AlertIcon,
        color: colors.yellow[500],
        message: 'This is a warning'
      }
    case 'hidden':
      return {
        icon: CheckIcon,
        color: '',
        message: ''
      }
    default:
      throw new Error();
  }
}
import React from 'react';
import { publishMessageService } from 'some-services';
import { FeedbackLabelComponent } from 'some-components';
import { reducer } from './methods'
import { StateProps } from './props'

const initialState: StateProps = {
  icon: CheckIcon,
  color: '',
  message: ''
};

export const PublishMessageScreen: React.FC = () => {
  /**
   * STATES
   */
  const [feedbackState, dispatch] = useReducer(reducer, initialState);

  async function handlePublishMessage(message) {
    try {
      const response = await publishMessageService(message);
      dispatch({ mode: 'success' })
    } catch (error) {
      dispatch({ mode: 'error' })
    }
  }

  return (
    <div>
      <FeedbackLabelComponent
        {...feedbackState}
      />
      <button onClick={() => handlePublishMessage('Example message'))}>
        Publish message
      </button>
    </div>
  );
};

Conclusion

If you want to see the complete source code of the project, click here to access the Github repository.

Thanks for reading!

References

React's official documentation

Rui Fernandes

Rui Fernandes

Focused Full Stack Engineer creating impactful digital solutions.