useReducer by example

Machine Gurning
5 min readSep 24, 2022

--

The useReducer hook, as explained by the React documentation, is simply an alternative to the useState hook that enables more complex state management in React. This article will attempt to convey its functionality by means of vaguely useful examples.

Initial set-up

I will be using typescript. Feel free to follow-along by running the following terminal commands:

npx create-react-app --template typescript use-reducer-examplecd use-reducer-example npm start

To avoid physical nausea, I will quickly implement some CSS styling, using the styled-components package, which you can read about here and listen to someone talk about here. For brevity’s sake, I recommend simply mindlessly following these instructions. If you really must insist, then a brief explanation of what I’m doing is

  1. Installing a library that will make it easier to style components
  2. Making some components for our little counter: the page itself, a container for the counter, the counter visualisation itself, the buttons that will increase or decrease the counter’s value
  3. Placing those components in the app

In another terminal window (keeping your first one running), execute this command:

yarn add styled-components @types/styled-components

Now create a file called styles.ts in the src directory of the project, and paste this:

import styled from 'styled-components';

export const AppContainer = styled.div`
align-items: center;
justify-content: center;
background-color: lightgrey;
display: flex;
flex-direction: row;
height: 80vh;
padding: 10vh 10vw;
width: 80vw;
`
export const CounterContainer = styled.div`
align-items: center;
flex-direction: column;
background-color: white;
display: flex;
padding: 20px;
height: 400px;
width: 500px;
`

export const CounterValue = styled.div`
font-size: 200px;
text-align: center;
`

export const ControlsContainer = styled.div`
padding: 10px;
margin-top: 50px;
`

export const CounterButton = styled.button`
background-color: #5aac44;
font-size: 50px;
margin: 10px;
border-radius: 3px;
border: none;
box-shadow: none;
color: #fff;
padding: 6px 12px;
text-align: center;
cursor: pointer;
`

In the file App.tsx , replace everything with this:

import React from 'react';
import './App.css';
import {AppContainer, CounterContainer, CounterValue, ControlsContainer, CounterButton} from "./styles";

function App() {
return (
<AppContainer>
<CounterContainer>
<CounterValue>0</CounterValue>
<ControlsContainer>
<CounterButton>
Decrease
</CounterButton>
<CounterButton>
Increase
</CounterButton>
</ControlsContainer>
</CounterContainer>
</AppContainer>
);
}


export default App;

Upon saving, your localhost:3000 page shoud look like this

1. A simple counter

I will start by creating a simple web-page with a counter by using useState, then refactor it to make use of useReducer. This should demonstrate the similarities and notable differences of these two hooks. I presume you have some familiarity with the former.

Using useState

We wish increase or decrease the value of the counter by clicking the respective buttons. We can do this by using useState:

In App.tsx:

import React, {useState} from 'react';....function App() {
const [counterValue, setCounterValue] = useState(0);
....<CounterValue>{counterValue}</CounterValue>
<ControlsContainer>
<CounterButton onClick={
() => setCounterValue(counterValue - 1)
}
>
Decrease
</CounterButton>
<CounterButton onClick={
() => setCounterValue(counterValue + 1)
}
>
Decrease
</CounterButton>
....

Now the counter is controlled by the buttons.

Using useReducer

We may employ useReducer to achieve the same outcome. Though it will result in a more complicated control mechanism for this simple widget, the goal here is to convey an understanding of the hook. After we refactor this successfully, we will make some incremental changes to the application to demonstrate the real value of our new method.

Instead of maintaining the state of a single number, the useReducer hook allows us to track the state of larger, more complex objects with multiple fields.

  • We start with an “initial state”, and then use a function (called a reducer function) to make an updated copy of that state upon our instruction
  • The reducer function takes as inputs the current state, and a ‘payload’ of instructions for how to alter the current state.
  • In our case, the initial state will be { counter: 0 } and the payload will either be { action: "INCREASE" } or { action: "DECREASE" } depending on which button is pressed.
  • Our reducer function will therefore behave like this: If I receive an action of “INCREASE”, I will return an object with one added to the current counter value. If I receive an action of “DECREASE”, I will return an object with one subtracted from the current counter value

In code, these instructions look like the block below. Note that I have also defined types for the counter and payload objects, just to be cute:

// Type for the counter object
interface counterType {
counter: number
}
// Type for the payload object
interface payloadType {
action: "INCREASE" | "DECREASE"
}
// Reducer function
const counterReducer = (state: counterType, payload: payloadType): counterType => {
switch (payload.action) {
case "INCREASE":
return { counter: state.counter + 1}
case "DECREASE":
return { counter: state.counter - 1}
default:
return {...state}
}
}

To use this reducer, we must now add this line of code to our component:

const [state, dispatch] = useReducer( counterReducer, {counter: 0} )

The useReducer hook takes as arguments the reducer function we just defined, and the initial state. It returns the state and a dispatch function, which is simply what you will use to call the reducer function and thereby change the state. For example, if I wanted to increase the state by one, I could write dispatch({ action: "INCREASE" })

Now we can refactor the rest of our application to make use of the new reducer function:

import React, {useReducer, useState} from 'react';....// Type for the counter object
interface counterType {
counter: number
}
// Type for the payload object
interface payloadType {
action: "INCREASE" | "DECREASE"
}
// Reducer function
const counterReducer = (state: counterType, payload: payloadType): counterType => {
switch (payload.action) {
case "INCREASE":
return { counter: state.counter + 1}
case "DECREASE":
return { counter: state.counter - 1}
default:
return {...state}
}
}

function App() {
// initialising the state
const [state, dispatch] = useReducer(counterReducer,{counter: 0})


return (
<AppContainer>
<CounterContainer>
<CounterValue>{state.counter}</CounterValue>
<ControlsContainer>
<CounterButton onClick={ ()=> dispatch({action: "DECREASE"})}>
Decrease
</CounterButton>
<CounterButton onClick={ ()=> dispatch({action: "INCREASE"})}>
Increase
</CounterButton>
</ControlsContainer>
</CounterContainer>
</AppContainer>
);
}

export default App;

Having sufficiently proven to myself my understanding of the useReducer function, I will leave you now.

--

--