Simplifying State Management with useReducer on React.js apps
by Rui Fernandes -

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:
- Basic React knowledge;
- Basic TypeScript knowledge.
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:
- icon;
- color (label background color);
- message (label text);
- visible (boolean value that indicates if the component is visible or not). Default: true);
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:
- reducer;
- initialState;
- initializer (optional)
This is the useReducer call:
const [state, dispatch] = useReducer(reducer, initialState);
This hook returns a two-position array as the useState:
- state: current state
- 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:
- State: current state
- Action: action that will be taken after calling the dispatch function.
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:
- props.ts file
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;
}
- methods.ts file:
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();
}
}
- index.tsx file:
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!