How-to: React hooks: useReducer

What is useReducer in React Hooks?

“An alternative to useState.”

In the official React docs, useState is listed as basic hooks while useReducer is at additional hooks. Since useReducer is more powerful, it is usually used at more complex state logic.

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

See a simple example code below:

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')
 }
};

Now let’s just create a simple component, and what it does is to load more data into state from a dummy data set up (or a server if its available).

import React from "react";
import { makeStyles } from "@material-ui/core/styles";

const allData = new Array(25).fill(0).map((_val, i) => i + 1);
const perPage = 5;

function UseReducerDemo() {
  const [data] = React.useState(allData.slice(0, perPage));
  const classes = useStyles();

  return (
    <div className={classes.rootInput}>
      <ul>
        {data.map((row) => (
          <li key={row} style={{ background: "purple" }}>
            {row}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UseReducerDemo;

const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },

  rootInput: {
    "& > *": {
      margin: theme.spacing(1),
      width: "25ch",
    },
  },
}));

Currently, it is just grabbing the first 5 data from the array and passing it to the state [data], and then we come to our list and mapping it out which is we are seeing the first 5 elements on the screen above.

The next step is add a to say add more and it will go and fetch the next five elements until we’ve loaded all of the data from our data source.

To start with, we need to keep track of a number of state properties.

loading : Are we currently loading from our data source? We may need to show a message to the user that is something is happening.

more: A boolean to know if there are any more data or elements to be loaded.

after: A variable to keep track in terms of where we start at in terms of loading or reloading more data

data: the data itself which is currently in state

And this is the perfect use case for useReducer when we have to keep track of multiple state properties all at the same time, for example, when the user clicks the button โ€”which we are going to build next.

We can just dispatch an event that we’re loading data and the useReducer will keep track of managing all of these state properties.

First let’s start with our useReducer.

import React from "react";
import { makeStyles } from "@material-ui/core/styles";

const allData = new Array(25).fill(0).map((_val, i) => i + 1);
const perPage = 5;

//reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case "start":
      //TODO
    case "loaded":
      //TODO
    default:
      throw new Error("Don't understand the action");
  }
};


function UseReducerDemo() {

  const [data] = React.useState(allData.slice(0, perPage));

	const [state, dipatch] = React.useReducer(() => {}, {
	    loading: false,
	    more: true,
	    data: [],
	    after: 0

  })

...

Let’s trigger first the ‘start’ through the button that we will create. The button will listen for an onClick event.

...
return (
    <div className={classes.rootInput}>
      <ul>
        {data.map((row) => (
          <li key={row} style={{ background: "pink" }}>
            {row}
          </li>
        ))}
        <li style={{ background: "gray" }}>
          <button onClick={() => {
						dispatch({ type: "start" });
							}}> Load More</button>
        </li>
      </ul>
    </div>
  );
}
...

let’s edit our reducer function.

//reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case "start":
      return { ...state, loading: true };    //returns the new value of the state when the button is clicked
    case "loaded":
      break;
    default:
      throw new Error("Don't understand the action");
  }
};

And also our function component.

...

function UseReducerDemo() {
  const [state, dispatch] = React.useReducer(reducer, {
    loading: false,
    more: true,
    data: [],
    after: 0,
  });

  const { loading, data } = state;
  const classes = useStyles();

  return (
    <div className={classes.rootInput}>
      <ul>
        {data.map((row) => (
          <li key={row} style={{ background: "pink" }}>
            {row}
          </li>
        ))}

      {loading && <li>Loading...</li>}   

        {!loading && (
          <li style={{ background: "gray" }}>
            <button
              onClick={() => {
                dispatch({ type: "start" });
              }}
            >
              Load More
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}


...

Next, let’s dispatch another event when the data has been loaded. But first, we need to grab the new data.

/* dispatch another event when the data has bee loaded. Let's wrap it in setTimeout */ 

               setTimeout(() => {
                  const newData = allData.slice(after, after + perPage);
                  dispatch({ type: "loaded", newData });
                }, 1000);
              }}

/* don't forget to extract or update the state */
const { loading, data, after } = state;

Let’s update the switch case in the reducer function.

case "loaded":
      return {
        ...state,
        loading: false,
        data: [...state.data, ...action.newData], //data will contain the new data plus the new data
        more: action.newData.length === perPage, //to check if there are more data to be loaded
        after: state.after + action.newData.length,
      };

Finally, we want to use our more boolean and whether we want to show the <load more> button as long as there’s more to load.

/* don't forget to extract or update the state */
const { loading, data, after, more } = state;



....
{loading && <li>Loading...</li>}

        {!loading && more && (
          <li style={{ background: "gray" }}>
            <button
....

๐Ÿ“ŒSo that’s it. Let’s briefly review why we use useReducer.

๐Ÿ‘‰๐Ÿผ We had to keep track of nested state where we’re updating four properties.

๐Ÿ‘‰๐Ÿผ These properties are updated sort of all at the same time.

import React from "react";
import { makeStyles } from "@material-ui/core/styles";

const allData = new Array(25).fill(0).map((_val, i) => i + 1);
const perPage = 5;

//reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case "start":
      return { ...state, loading: true };
    case "loaded":
      return {
        ...state,
        loading: false,
        data: [...state.data, ...action.newData],
        more: action.newData.length === perPage,
        after: state.after + action.newData.length,
      };
    default:
      throw new Error("Don't understand the action");
  }
};

function UseReducerDemo() {
  const [state, dispatch] = React.useReducer(reducer, {
    loading: false,
    more: true,
    data: [],
    after: 0,
  });

  const { loading, data, after, more } = state;
  const classes = useStyles();

  return (
    <div className={classes.rootInput}>
      <ul>
        {data.map((row) => (
          <li key={row} style={{ background: "pink" }}>
            {row}
          </li>
        ))}

        {loading && <li>Loading...</li>}

        {!loading && more && (
          <li style={{ background: "gray" }}>
            <button
              onClick={() => {
                dispatch({ type: "start" });

                /* dispatch another event when the data has bee loaded*/
                setTimeout(() => {
                  const newData = allData.slice(after, after + perPage);
                  dispatch({ type: "loaded", newData });
                }, 1000);
              }}
            >
              Load More
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}