Understanding state management in Next.js
Tags: nextjs
Published: 2022-04-20 16:39:20

**Written by [Madars Bišs](https://blog.logrocket.com/author/madarsbiss/)✏️**

State management is the core of any modern web application, as it determines what data is displayed on the screen during the app usage session while the user interacts with it.

For example, ticking checkboxes in online surveys, adding products to a shopping cart on an e-commerce store, or selecting audio from a playlist in a music player is possible thanks to state management keeping track of each action the user makes.

In this article, we will review many state management methods that you can use to keep track of the states in your [Next.js](https://nextjs.org) applications. For each solution, I will provide a practical example so that it is easy to understand how each approach works.

We will use the top-to-bottom approach, reviewing the simplest methods first and moving into more advanced solutions for more complex use cases.

## How does a state work?

A state is a JavaScript object that holds the current status of the data. You can think of it as a light switch that can have either “on” or “off” states. Now, transfer the same principle to the React ecosystem and imagine the use of the light and dark mode toggle.

Each time the user clicks on the toggle, the opposite state is being activated. That state is then being updated in the JavaScript state object, so your application knows which state is currently active and what theme to display on the screen.

Regardless of how the application manages its data, the state must always be passed down from the parent element to the children elements.

## Understanding Next.js file structure

Because Next.js is a framework, it follows a specific file structure. To review different ways to store the states, we first need to understand how the Next.js file system is built.

If you run `npx create-next-app project-name` in your terminal, it will create a fully working Next.js application that consists of four main blocks: root level, then the `pages`, `public`, and `styles` folders in it.

For managing states, we will only use `pages` and the two files inside it: `_app` and `index.js`. The first is the root file for the entire application, where all the globally accessed components are configured. The latter is the base route file for the `Home` component.

There is also an `api` folder inside the `pages` folder that is built in and how Next.js handles the creation of API endpoints, allowing them to receive requests and send responses. We will work with this folder toward the end of the tutorial.

## Using the `useState` Hook for state management in Next.js

One of the most common ways to manage state is with the `useState` Hook. We will build an application that lets you increase the score by clicking the button.

Navigate into `pages` and include the following code in `index.js`:

```js
import { useState } from "react";

export default function Home() {
  const [score, setScore] = useState(0);
  const increaseScore = () => setScore(score + 1);

  return (
    <div>
      <p>Your score is {score}</p>
      <button onClick={increaseScore}>+</button>
    </div>
  );
}
```

We first imported the `useState` hook itself, then set the initial state to be `0`. We also provided a `setScore` function so we can update the score later.

Then we created the function `increaseScore`, which accesses the current value of the score and uses `setState` to increase that by `1`. We assigned the function to the `onClick` event for the **plus** **button**, so each time the button is pressed, the score increases.

## The `useReducer` Hook

The `useReducer` Hook works similarly to the reduce method for arrays. We pass a reducer function and an initial value. The reducer receives the current state and an action and returns the new state.

We will create an app that lets you multiply the currently active result by `2`. Include the following code in `index.js`:

```js
import { useReducer } from "react";

export default function Home() {
  const [multiplication, dispatch] = useReducer((state, action) => {
    return state * action;
  }, 50);
  return (
    <div>
      <p>The result is {multiplication}</p>
      <button onClick={() => dispatch(2)}>Multiply by 2</button>
    </div>
  );
}
```

First, we imported the `useReducer` Hook itself. We passed in the reducer function and the initial state. The Hook then returned an array of the current state and the dispatch function.

We passed the dispatch function to the `onClick` event so that the current state value gets multiplied by `2` each time the button is clicked, setting it to the following values: `100`, `200`, `400`, `800`, `1600`, and so on.

## The prop drilling technique for state management in Next.js

In more advanced applications, you will not work with states directly in a single file. You will most likely divide the code into different components, so it is easier to scale and maintain the app.

As soon as there are multiple components, the state needs to be passed from the parent level to the children. This technique is called [prop drilling](https://blog.logrocket.com/the-upsides-of-prop-drilling-in-react/), and it can be multiple levels deep.

For this tutorial, we will create a basic example just two levels deep to give you an idea of how the prop drilling works. Include the following code to the `index.js` file:

```js
import { useState } from "react";

const Message = ({ active }) => {
  return <h1>The switch is {active ? "active" : "disabled"}</h1>;
};

const Button = ({ onToggle }) => {
  return <button onClick={onToggle}>Change</button>;
};

const Switch = ({ active, onToggle }) => {
  return (
    <div>
      <Message active={active} />
      <Button onToggle={onToggle} />
    </div>
  );
};

export default function Home() {
  const [active, setActive] = useState(false);
  const toggle = () => setActive((active) => !active);

  return <Switch active={active} onToggle={toggle} />;
}
```

In the code snipped above, the `Switch` component itself does not need `active` and `toggle` values, but we have to "drill" through the component and pass those values to the children components `Message` and `Button` that need them.

## Using the Context API in Next.js

The `useState` and `useReducer` Hooks, combined with the [prop drilling technique](https://blog.logrocket.com/solving-prop-drilling-react-apps/), will cover many use cases for most of the basic apps you build.

But what if your app is way more complex, the props need to be passed down multiple levels, or you have some states that need to be accessible globally?

Here, it is recommended to avoid prop drilling and use the [Context API](https://reactjs.org/docs/context.html), which will let you access the state globally.

It's always a great practice to create separate contexts for different states like authentication, user data, and so on. We will create an example for theme state management.

First, let's create a separate folder in the root and call it `context`. Inside, create a new file called `theme.js` and include the following code:

```js
import { createContext, useContext, useState } from "react";

const Context = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <Context.Provider value={[theme, setTheme]}>{children}</Context.Provider>
  );
}

export function useThemeContext() {
  return useContext(Context);
}
```

We first created a new `Context` object, created a `ThemeProvider` function, and set the initial value for the `Context` to `light`.

Then, we created a custom `useThemeContext` Hook that will allow us to access the theme state after we import it into the individual pages or components of our app.

Next, we need to wrap the `ThemeProvider` around the entire app, so we can access the state of the theme in the entire application. Head to the `_app.js` file and include the following code:

```js
import { ThemeProvider } from "../context/theme";

export default function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}
```

To access the state of the theme, navigate to `index.js` and include the following code:

```js
import Link from "next/link";
import { useThemeContext } from "../context/theme";

export default function Home() {
  const [theme, setTheme] = useThemeContext();
  return (
    <div>
      <h1>Welcome to the Home page</h1>
      <Link href="/about">
        <a>About</a>
      </Link>
      <p>Current mode: {theme}</p>
      <button
        onClick={() => {
          theme == "light" ? setTheme("dark") : setTheme("light");
        }}
      >
        Toggle mode
      </button>
    </div>
  );
}
```

We first imported `useThemeContext`, then accessed the `theme` state and the `setTheme` function to update it when necessary.

Inside the `onClick` event of a toggle button, we created an update function that switches between the opposite values between `light` and `dark`, depending on the current value.

## Accessing `Context` via routes

Next.js uses a `pages` folder to create new routes in your app. For example, if you create a new file `route.js`, and then refer to it from somewhere via the `Link` component, it will be accessible via `/route` in your URL.

In the previous code snippet, we created a route to the `About` route. This will allow us to test that the theme state is globally accessible.

This route currently does not exist, so let's create a new file called `about.js` in the `pages` folder and include the following code:

```js
import Link from "next/link";
import { useThemeContext } from "../context/theme";

export default function Home() {
  const [theme, setTheme] = useThemeContext();
  return (
    <div>
      <h1>Welcome to the About page</h1>
      <Link href="/">
        <a>Home</a>
      </Link>
      <p>Currently active theme: {theme}</p>
      <button
        onClick={() => {
          theme == "light" ? setTheme("dark") : setTheme("light");
        }}
      >
        Toggle mode
      </button>
    </div>
  );
}
```

We created a very similar code structure that we used in the `Home` route earlier. The only differences were the page title and a different link to navigate back to `Home`.

Now, try to toggle the currently active theme and switch between the routes. Notice that the state is preserved in both routes. You can further create different components, and the theme state will be accessible whenever in the app file tree it is located.

## Data fetching from an API

The previous methods would work when managing states internally in the app. However, in a real-life scenario, you will most likely fetch some data from outside sources via API.

The data fetching can be summarized as making a request to the API endpoint and receiving the data after the request is processed and the response is sent.

We need to take into consideration that this process is not immediate, so we need to manage states of the response, like the state of waiting time, while the response is being prepared. We’ll also handle the cases for potential errors.

Keeping track of the waiting state lets us display a loading animation to improve the UX, and the error state lets us know that the response was unsuccessful. It lets us display the error message, giving us further information about the cause.

## The Fetch API and the `useEffect` Hook

One of the most common ways to handle data fetching is to use the combination of the native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and the `useEffect` Hook.

The `useEffect` Hook lets us perform side effects once some other action has been completed. With it, we can track when the app has been rendered and we are safe to make a Fetch call.

To fetch the data in NextJS, transform the `index.js` to the following:

```js
import { useState, useEffect } from "react";

export default function Home() {
  const [data, setData] = useState(null)
  const [isLoading, setLoading] = useState(false)

  useEffect(() => {
    setLoading(true)
    fetch('api/book')
      .then((res) => res.json())
      .then((data) => {
        setData(data)
        setLoading(false)
      })
  }, [])

  if (isLoading) return <p>Loading book data...</p>
  if (!data) return <p>No book found</p>

  return (
    <div>
      <h1>My favorite book:</h1>
      <h2>{data.title}</h2>
      <p>{data.author}</p>
    </div>
  )
}
```

We first imported the `useState` and `useEffect` hooks. Then we created separate initial states for received data to `null` and loading time to `false`, indicating that no Fetch call has been made.

Once the app has been rendered, we set the state for loading to `true`, and create a Fetch call. As soon as the response has been received we set the data to the received response and set the loading state back to `false`, indicating that the fetching is complete.

Next, we need to create a valid API endpoint, so navigate to the `api` folder and create a new file called `book.js` inside it, so we get the API endpoint we included in the fetch call in the previous code snippet. Include the following code:

```js
export default function handler(req, res) {
  res
    .status(200)
    .json({ title: "The fault in our stars", author: "John Green" });
}
```

This code simulates a response about the book title and author you would normally get from some external API, but will be fine for‌ this tutorial.

## Using SWR for state management in Next.js

There is also an alternate method, created by the Next.js team itself, to handle data fetching in an even more convenient way.

It's called [SWR](https://swr.vercel.app/) — a custom hook library that handles caching, revalidation, focus tracking, re-fetching on the interval, and more. To install it, run `npm install swr` in your terminal.

To see it in action, let's transform the `index.js` file.

```js
import useSWR from "swr";

export default function Home() {
  const fetcher = (...args) => fetch(...args).then((res) => res.json());
  const { data, error } = useSWR("api/user", fetcher);

  if (error) return <p>No person found</p>;
  if (!data) return <p>Loading...</p>;

  return (
    <div>
      <h1>The winner of the competition:</h1>
      <h2>
        {data.name} {data.surname}
      </h2>
    </div>
  );
}
```

Using SWR simplifies many things: the syntax looks cleaner and is easier to read, it’s well suited for scalability, and errors and response states get handled in a couple of lines of code.

Now, let's create the API endpoint so we get the response. Navigate to the `api` folder, create a new file called `user.js,` and include the following code:

```js
export default function handler(req, res) {
  res.status(200).json({ name: "Jade", surname: "Summers" });
}
```

This API endpoint simulates the fetching of the name and surname of the person, which you would normally get from a database or an API containing a list of publicly available names.

## Conclusion

In this tutorial, we built several mini-applications to showcase many of the ways we can manage state in Next.js apps.

Each use case used different state management solutions, but the biggest challenge for picking the most appropriate state management solution is the ability to identify what states you need to track.

Beginners often struggle and choose overkill solutions for managing simple states, but it gets better with time, and the only way to improve it is by practicing. Hopefully, this article helped you to take a step in the right direction towards achieving that.

---

## [LogRocket](https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try [LogRocket](https://logrocket.com/signup). 

![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)

[LogRocket](https://logrocket.com/signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — [start monitoring for free](https://logrocket.com/signup).