Flawless React State Management: useReducer and Context API
Tags: react, javascript
Published: 2021-01-22 12:31:00

## ✨ Introduction

Adding state to a React application can be tricky, especially when it's starts to grow in size. On which level do you manage the state? Which components do you give local state only? What about state that you need to access everywhere? Redux is a great library for managing state but it can be overkill in a small to medium application, which you'll probably need to make quite often. 

In this tutorial we will build a small user management app that will teach you how to manage state in React with, in my opinion, currently the best way possible.

## 🎯 Goals

* Setting up a React app
* Using 100% React Hooks
* Using Context API
* Using useReducer hook
* Loading API data asynchronously into our state
* Adding theme toggle switch

## 📺 What we’ll make

[Click here to see the app live in action.](https://pensive-benz-7656bc.netlify.app/)
[Click hero to check out the github repo.](https://github.com/sanderdebr/react-usereducer-context-tutorial)

## 🔨 Setting up the application

Let's start by creating a new React app with create-react-app:
`npx create-react-app user-management`

I like to use Material UI or Tailwind in my React projects, let's use Material UI this time:
`npm install @material-ui/core`

And adding the Roboto font in our index.html:
`<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />`

As well as adding the icon set of Material UI:
`npm install @material-ui/icons`

Then let's remove all the files except index.js and App.js as we will not use those.

Now we'll create a basic layout and dashboard that will hold a list of our users. We will wrap every page in a `_layout.js` function that provides a theme and template. Inside `App.js` we'll add the standard react-router functionality:

**_layout.js**
```javascript
import { Box, Container, CssBaseline } from "@material-ui/core";
import React, { useState } from "react";

export default function Layout({ children }) {
  return (
    <>
      <CssBaseline />
      <Container maxWidth="md">
        <Box marginTop={2}>{children}</Box>
      </Container>
    </>
  );
}
```

**App.js**
```javascript
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";

import Layout from "./_layout";
import Users from "./Users";

function App() {
  return (
    <Layout>
      <Router>
        <Switch>
          <Route path="/">
            <Users />
          </Route>
          <Route path="/edit-user">
            <h1>Edit user</h1>
          </Route>
        </Switch>
      </Router>
    </Layout>
  );
}

export default App;
```

**Users.js**
```javascript
import {
  Button,
  Divider,
  Grid,
  Paper,
  Typography,
  makeStyles,
} from "@material-ui/core";

import Brightness7Icon from "@material-ui/icons/Brightness7";
import React from "react";
import UserList from "./UserList";

const useStyles = makeStyles((theme) => ({
  paper: {
    padding: theme.spacing(4),
    margin: "auto",
  },
  img: {
    width: "100%",
  },
  divider: {
    marginBottom: theme.spacing(2),
  },
}));

export default function Users() {
  const classes = useStyles();

  return (
    <Paper className={classes.paper}>
      <Grid container justify="space-between" alignItems="start">
        <Grid item>
          <Typography gutterBottom variant="h4">
            Users
          </Typography>
        </Grid>
        <Grid item>
          <Grid container spacing={4} alignItems="center">
            <Grid item>
              <Button variant="contained" color="primary">
                Load users
              </Button>
            </Grid>
            <Grid item>
              <Brightness7Icon />
            </Grid>
          </Grid>
        </Grid>
      </Grid>
      <Divider className={classes.divider} />
      <UserList />
    </Paper>
  );
}

```

Also I've added a default icon already for our theme switch that we will make later.

### Adding list of users

Let's now add cards that will hold our user information.

**UserList.js**
```javascript
import { Grid } from "@material-ui/core";
import React from "react";
import User from "./User";

export default function UserList() {
  const users = [1, 2, 3];

  return (
    <Grid container spacing={2}>
      {users.map((user, i) => (
        <Grid item xs={12} sm={6}>
          <User key={i} user={user} />
        </Grid>
      ))}
    </Grid>
  );
}
```

**User.js**
```javascript
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import React from "react";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles({
  media: {
    height: 140,
  },
});

export default function User() {
  const classes = useStyles();

  return (
    <Card className={classes.root}>
      <CardActionArea>
        <CardContent>
          <Typography gutterBottom variant="h5" component="h2">
            Clementine Bauch
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>ID: </strong> Samantha
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>Username: </strong> Samantha
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>Email: </strong> Nathan@yesenia.net
          </Typography>
        </CardContent>
      </CardActionArea>
      <CardActions>
        <Button size="small" variant="contained" color="secondary">
          Delete
        </Button>
        <Button size="small" variant="contained" color="primary">
          Edit
        </Button>
      </CardActions>
    </Card>
  );
}
```

We currently are just using some hard coded user data and an array with 3 items to show our users. In a further section we will load our users from an API and store them in our app state.

Before that, let's first create the theme switch using the Context API.

## 💡 Adding Context API

Create a new folder called `context` and in here add a folder called `theme`. Inside this folder we'll create the following 3 files: `context.js`, `index.js` and `reducer.js`.

I will explain each file step by step.

**context.js**
We'll be using React's Context API to wrap our app with some values which we would like to provide, in this case the theme settings. 

First we'll create a new context"
```javascript
const { createContext } = require("react");
const ThemeContext = createContext();`
```

Then we'll set up a wrapper function that provides the theme to our whole app:
```javascript
<ThemeContext.Provider theme={currentTheme} setTheme={setTheme}>
      {children}
</ThemeContext.Provider>
```

To make this work with Material UI, we have to pass the theme into their `createMuiTheme()` function. We'll use React's useState hook to get and set the state. We'll also provide the `setTheme` function into our context.

We can consume the context values anywhere in our app by using the `useContext()` hook:
`export const useTheme = () => useContext(ThemeContext);`

The whole context then looks like this:

```javascript
import React, { useContext } from "react";

import { createMuiTheme } from "@material-ui/core";

const { createContext } = require("react");

const ThemeContext = createContext();

export const useTheme = () => useContext(ThemeContext);

export const ThemeProvider = ({ children }) => {
  const dark = {
    palette: {
      type: "dark",
    },
  };

  const currentTheme = createMuiTheme(dark);

  return (
    <ThemeContext.Provider value={currentTheme}>
      {children}
    </ThemeContext.Provider>
  );
};
```

Next up we'll use `index.js` for easy importing our context files in other files:

**index.js**
```javascript
import { useTheme, ThemeProvider } from "./context";

export { useTheme, ThemeProvider };
```

And we'll wrap our App inside `App.js` with the provider:

**App.js**
```javascript
...
function App() {
  return (
    <ThemeProvider>
         ...
    </ThemeProvider>
  );
}
...
```

We'll update the `_layout.js` file so that we can provide our theme with material UI:

**_layout.js**
```javascript
import {
  Box,
  Container,
  CssBaseline,
  ThemeProvider,
  createMuiTheme,
} from "@material-ui/core";

import React from "react";
import { useThemeState } from "./context/theme";

export const light = {
  palette: {
    type: "light",
  },
};

export const dark = {
  palette: {
    type: "dark",
  },
};

export default function Layout({ children }) {
  const { theme } = useThemeState();

  const lightTheme = createMuiTheme(light);
  const darkTheme = createMuiTheme(dark);

  return (
    <ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
      <CssBaseline />
      <Container maxWidth="md">
        <Box marginTop={2}>{children}</Box>
      </Container>
    </ThemeProvider>
  );
}
```

Now we can use the theme anywhere in our app by using the `useTheme()` hook. For example inside `Users.js` we can add the following to show a sun or moon based on our theme setting:

```javascript
const theme = useTheme();
{theme.palette.type === "light" ? 
<Brightness7Icon /> : <Brightness4Icon />}
```

This is super helpful, we added a global state to our app! But what if we wanted to update that state? That's where the useReducer comes into the picture.

## Adding useReducer

The React useReducer hook is an alternative to useState. It acceps a function that mutates the state object, and an initial state object.

The useReducer hook returns the state and a dispatch function, which we can use to fire off changes to our state. It is similar as how Redux works, but less complicated. (I still recommended to learn Redux down the road as it has more benefits for more complex applications).

Because not all components need to access the state and dispatch we will split them up into 2 contexts.

Our new `context.js` file then looks like the following:

**context.js**
```javascript
import React, { useContext, useReducer } from "react";

import { themeReducer } from "./reducer";

const { createContext } = require("react");

const initialState = {
  switched: 0,
  theme: "light",
};

const ThemeStateContext = createContext();
const ThemeDispatchContext = createContext();

export const useThemeState = () => useContext(ThemeStateContext);
export const useThemeDispatch = () => useContext(ThemeDispatchContext);

export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={dispatch}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
};
```

Awesome, next up let's create our first reducer called `themeReducer` in the file `reducer.js`.

**reducer.js**
```javascript
export const themeReducer = (state, { type }) => {
  switch (type) {
    case "TOGGLE_THEME":
      return {
        ...state,
        switched: state.switched + 1,
        theme: state.theme === "light" ? "dark" : "light",
      };
    default:
      throw new Error(`Unhandled action type: ${type}`);
  }
};
```

The function above updates the state when an action with label "TOGGLE_THEME" comes in. If the action is unknown, it will throw an error.

We'll also update our initial state and themes inside our context.js file:

**context.js**
```javascript
import React, { useContext, useReducer } from "react";

import { createMuiTheme } from "@material-ui/core";
import { themeReducer } from "./reducer";

const { createContext } = require("react");

export const light = {
  palette: {
    type: "light",
  },
};

export const dark = {
  palette: {
    type: "dark",
  },
};

export const lightTheme = createMuiTheme(light);
export const darkTheme = createMuiTheme(dark);

const initialState = {
  switched: 0,
  theme: lightTheme,
};

const ThemeStateContext = createContext();
const ThemeDispatchContext = createContext();

export const useThemeState = () => useContext(ThemeStateContext);
export const useThemeDispatch = () => useContext(ThemeDispatchContext);

export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={dispatch}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
};
```

Now we can use both `switched` and `theme` anywhere in our application with: `const { theme } = useThemeState()`. Very cool!

### Creating the theme toggle

Inside users.js we can now use our dispatch functionality:

**Users.js**
```javascript
....
const { theme } = useThemeState();
const dispatch = useThemeDispatch();
...
<Grid item onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
              {theme === "light" ? <Brightness7Icon /> : <Brightness4Icon />}
            </Grid>
```

Our theme toggle is working, awesome!

## Loading users from an API

Let's create a new folder inside our context folder and call it `users` and add the same files as in `theme` but now also add `actions.js` to it.

We'll repeat the same code as we did for the theme context, except we will add `actions.js` this time as we want to perform an API fetch and based on the result update our state. Our reducer only should be concerned with updated the state directly, performing actions we will keep seperate just like Redux does.

**actions.js**
```javascript
export const getUsers = async (dispatch) => {
  dispatch({ type: "REQUEST_USERS" });
  try {
    // Fetch server
    const response = await fetch(`https://jsonplaceholder.typicode.com/users`);

    if (!response.ok) {
      throw Error(response.statusText);
    }

    let data = await response.json();

    // Received users from server
    if (data.length) {
      dispatch({ type: "USERS_SUCCESS", payload: data });
      return data;
    }

    // No match found on server
    dispatch({
      type: "USERS_FAIL",
      error: { message: "Could not fetch users" },
    });

    return null;
  } catch (error) {
    dispatch({ type: "USERS_FAIL", error });
  }
};
```

When the function above gets called it will fetch user data from an API endpoint. "REQUEST_USERS" will set our state to `loading: true`. If users are returned, we'll update our state with them in our reducer, if not we'll also update our state error object:

**reducer.js**
```javascript
export const usersReducer = (state, { type, payload, error }) => {
  switch (type) {
    case "REQUEST_USERS":
      return {
        ...state,
        loading: true,
      };
    case "USERS_SUCCESS":
      return {
        ...state,
        loading: false,
        users: payload,
      };
    case "USERS_FAIL":
      return {
        ...state,
        loading: false,
        error,
      };
    default:
      throw new Error(`Unhandled action type: ${type}`);
  }
};
```

Now it is up to you to wrap the users context around the application. You can do it the same way as we did for the theme.

Let's fetch our users when the user clicks on our 'LOAD_USERS' button by dispatching the right action:

**Users.js**
```javascript
...
  const dispatchUsers = useUsersDispatch();
  const _getUsers = () => getUsers(dispatchUsers);
...
<Button onClick={_getUsers} variant="contained" color="primary">
                Load users
              </Button>
...
```

Now we can fetch users and save them in the state, let's show them in our application:

**UserList.js**
```javascript
import { Grid } from "@material-ui/core";
import React from "react";
import User from "./User";
import { useUsersState } from "../context/users";

export default function UserList() {
  const { users, loading, error } = useUsersState();

  if (loading) {
    return "Loading...";
  }

  if (error) {
    return "Error...";
  }

  return (
    <Grid container spacing={2}>
      {users?.map((user, i) => (
        <Grid key={i} item xs={12} sm={6}>
          <User user={user} />
        </Grid>
      ))}
    </Grid>
  );
}
```

You can of course add some awesome loading spinners or display a better error, but hopefully you see how easy it is to load the app state wherever you need it and update the UI accordingly.

I would like to invite you to add the delete functionality! By adding a dispatch function to the delete button and removing the users inside the reducer based on its id.

Here is the code:

**User.js**
```javascript
...
const dispatch = useUsersDispatch();
...
 <Button
          onClick={() => dispatch({ type: "DELETE_USER", payload: user.id })}
          size="small"
          variant="contained"
          color="secondary"
        >
          Delete
        </Button>
```

**reducer.js**
```
case "DELETE_USER":
      return {
        ...state,
        users: state.users.filter((user) => user.id !== payload),
      };
```

## Persisting our state

One last thing we can do to improve our app is maintaining the app state when the user closes the window. This can be done by storing our states inside the users local storage and is called persisting the state.

First we'll add our state to the local storage every time our state changes inside our context.js files:

**context.js**
```javascript
export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  // Persist state on each update
  useEffect(() => {
    localStorage.setItem("theme", JSON.stringify(theme));
  }, [theme]);

  return ( ...
```

Then we'll change our initialstate to grab the state stored in local storage when it is available, otherwise use the initial state we declared already.

Instead of the initial state we'll pass in a initializer function into our reducers:

**reducer.js**
```javascript
...
const initialState = {
  loading: false,
  error: null,
  users: [],
};

const initializer = localStorage.getItem("users")
  ? JSON.parse(localStorage.getItem("users"))
  : initialState;
...
const [state, dispatch] = useReducer(usersReducer, initializer);
```

We'll do this for both contexts.

You should see your apps state in the local storage of your browser, awesome! 🔥

[Click here to see the app live in action.](https://pensive-benz-7656bc.netlify.app/)
[Click hero to check out the github repo.](https://github.com/sanderdebr/react-usereducer-context-tutorial)

There are so many possibilities with these technologies, I hope this tutorial will help you in any kind of way!