Caching in Next.js with unstable_cache
Tags: nextjs, webdev
Published: 2024-05-22 14:25:43

**Written by [Kapeel Kokane](https://blog.logrocket.com/author/kapeelkokane/)✏️**

In programming, caching is when you store the results of an expensive operation so that you can reuse them later. This can be useful when a function takes a long time to run or when a function is called multiple times with the same input. 

The same concept can be extended to web development. Caching can be applied to the response of an API call or even a whole page being returned from the server. In this article, we will learn how to cache data in Next.js using the `unstable_cache` method introduced in Next.js v14\. We will also compare the Next.js cache method with the React cache method to understand the use cases in which you would use one over the other.

## Introduction to `unstable_cache`

The `unstable_cache` function is a new addition to the Next.js cache API. Next.js already had several high-level APIs that we will discuss later in this article. But `unstable_cache` is a low-level cache API that allows you more granular control over the caching. 

It is important to note that this API is still unstable and its contract might change in the future. Nonetheless, it is a powerful tool that can be used to cache the results of database queries or API calls. 

Here's an example of how the `unstable_cache` API can be used:

```javascript
const data = unstable_cache(func, keys, options)()
```

*   `func` is the function that we want to cache. It must always return a promise
*   `keys` is an array of globally unique values that together identify the key of the data being cached
*   `options` is an object that lets us control some more granular aspects of the caching, like adding `tags` that can be used to later invalidate the cache. Or passing a `revalidate` parameter, which is the number of seconds after which the cache should be revalidated

Notice the `()` at the end of the function call. This is because `unstable_cache` returns a function that we need to call to get the cached data.

## The need for the `unstable_cache` API

You might be wondering why we need a low-level cache API when Next.js already provides [high-level caching mechanisms](https://nextjs.org/docs/app/building-your-application/caching) mentioned in the documentation. Let us first understand those mechanisms so that we can appreciate what `unstable_cache` brings to the table.

### Request memoization

With [request memoization](https://nextjs.org/docs/app/building-your-application/caching#request-memoization), the `fetch` API automatically memoizes requests that have the same URL and options during a render cycle of the React component tree. 

Let’s say we have a server-rendered React component called `Parent` that looks something like this:

```javascript
export default function Parent() {
  return (
    <div>
      <Child/>
      <Child/>
      <Child/>
    </div>
  )
}
```

Say that the child component has some code to make an API call like this:

```javascript
async function getData() {
  const res = await fetch('https://.../data')
  return res.json()
}
const item = await getData() 
```

Notice that there are three `Child` components inside the `Parent` component. This means that the function `getData` will be called three times. But, because of request memoization, the `fetch` API will only make the API call once and return the same result for all the calls without the need for any explicit configuration. That is because the rendering of these three child components is happening in the same render cycle of the React component tree.

### Data Cache

Then, there is the [Data Cache](https://nextjs.org/docs/app/building-your-application/caching#data-cache) that persists API results across server requests. Caching is enabled by default, and we can configure it in a more granular manner using the `cache` option in the `fetch` request. This is different from request memoization because the Data Cache persists the results across server requests. 

Continuing from our previous example, if we refresh the Parent component page after its initial render, the API call won't reach out to the original data source (`'https://.../data'`). Instead, it returns the cached result from the Data Cache, which was filled during the previous request. To prevent this default behavior and fetch from the original data source, we can pass the option `{ cache: 'no-store' }` to the fetch request.

### Full Route Cache

The Full Route Cache is the type of caching wherein Next.js automatically renders and caches routes at build time. To understand this, we need to understand the steps that take place every time a component is rendered in Next.js:

*   React converts Server Components into a special data format called React Server Component Payload
*   Next.js uses the React Server Component Payload to render the resulting HTML on the server
*   Next.js then caches these results, which include both the HTML and the React Server Component Payload for that particular route
*   The HTML is sent to the client where a non-interactive preview of the components is displayed, which is later hydrated to make the application interactive

Thus, a static route gets cached in the Data Cache at build time and a dynamic route gets cached the first time it is accessed.

### **Router Cache**

The Router Cache is an in-memory version of the cache that is stored on the client (browser) as opposed to the Full Route Cache, which gets stored on the server. An interesting thing to note is that the Router Cache is split by route segments. This means that if we have a common layout for pages A and B, then the layout gets cached when either of these two components is rendered. The layout does not need to be rendered the second time because it has already been cached.

### **`unstable_cache` API**

With all of that understood, we can now learn where the `unstable_cache` API fits in. In our previous examples, the common underlying assumption was that all the data fetching would happen via the `fetch` API. As the `fetch` API is already modified by Next.js to support memoization, we did not have to do much apart from configuring a few options. 

But, what about the cases where our external data comes from some DB clients or GraphQL clients? In those cases, we would not be able to use the `fetch` API and that is when a function like `unstable_cache` is necessary to wrap the database call function and explicitly memoize it. 

Right now, the Next.js official documentation suggests using the React [`cache` function](https://nextjs.org/docs/app/building-your-application/caching#react-cache-function) for this purpose because the Next.js cache API is still unstable. In a later section, we will cover the differences between the Next.js cache API and the React cache API.

## An example of the `unstable_cache` API

In this section, we will follow a hands-on example to better understand how the `unstable_cache` API works. We'll use the Next.js CLI to create a fresh project:

```yarn
npx create-next-app@latest nextjs-cache-example
```

Answer the prompts and then navigate to the project directory to start the dev server:

```yarn
npm run dev
```

This brings up the Next.js dev server on `[http://localhost:3000](http://localhost:3000)`, which looks like this:

![Next.js Homepage](https://blog.logrocket.com/wp-content/uploads/2024/05/next-js-homepage.png)

Create a new folder inside the `app` directory called `tasks`. This folder will hold the `page.js` that links to the `/user` route. This route will show information related to a user including their ID, name, and email.

### Creating a KV store

To test the capabilities beyond the `fetch` API, we will be creating a key-value store using Vercel storage. You can follow this [quickstart guide](https://vercel.com/docs/storage/vercel-kv/quickstart) on the official Vercel documentation to create a KV store connected to your project. 

After creating the store and connecting it with the project, we will use the CLI from the **Storage** menu of the project. We will run this command to create a hash key-value pair:

```javascript
hset user:123 email email@me.com id 123 name "Test User"
```

### Setting up the page

We will now set up the `/user` route to fetch the data from the KV store. Once we have that JSON, we will map over the keys to show the key-value pairs on the page. This is what the component code inside the `page.js` file looks like:

```javascript
import styles from "../page.module.css";
import { kv } from '@vercel/kv';

export function getUser() {
  return kv.hgetall('user:123');
}

export default async function User() {
  const user = await getUser();

  return (
    <main className={styles.main} >
      <div className={styles.description}>
        {Object.keys(user).map((key) => (
          <div key={key}>
            <code className={styles.code}>{key}:</code> 
            {user[key]}
          </div>
        ))}
      </div>
    </main>
  )
}
```

This is the information it displays based on the data that we added to the KV store:

![Key Value Store](https://blog.logrocket.com/wp-content/uploads/2024/05/key-value-store.png)

### Creating an edit page

We will now create an edit page, which will be a client-side route and will have a single button to allow us to edit user information. 

First, create a `page.js` file inside the `edit` folder inside the `app` directory. This is what the component code looks like:

```javascript
'use client'
import styles from "../page.module.css";
import { editUser } from '../actions.js';

export default function Tasks() {
  return (
    <main className={styles.main} >
      <div className={styles.description}>
        <button className={styles.button} onClick={async () => {
          await editUser()
        }}>Edit User</button>
      </div>
    </main>
  )
}
```

Notice the `'use client'` at the top of the component that makes this a client-side route and allows us to configure action handlers like the `onClick` handler. This is how the page looks, with just a single button:

![Edit User Button](https://blog.logrocket.com/wp-content/uploads/2024/05/edit-user-button.png)

We can also see that we are triggering a server action called `editUser` when the button is clicked. This is what the `actions.js` file looks like:

```javascript
'use server'
import { kv } from '@vercel/kv';

export async function editUser() {
  try {
    await kv.hset('user:123', { id: 123, email: 'test@me.com', name: 'Test User' });
  } catch (error) {
    console.log('error while editing user')
  }
}
```

Notice the `'use server'` at the top of the page. Also, notice how we can directly write code that talks to the KV store as we are sure this code will run on the server. 

We have configured the code such that when we click the **Edit User** button, the email of the user will change to `test@me.com` and we are expecting it to reflect on the `/user` page. 

But when we click the button and visit the `/user` page, we see that the email has not changed. This is because of the Full Route Cache that we mentioned earlier. Here's a GIF that shows the edit action and the user page:

![Editing The User](https://blog.logrocket.com/wp-content/uploads/2024/05/editing-user.gif)

### Fixing the caching issue

Next.js has a utility that allows us to fix this issue. The utility is called [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) and it is exported from the `next/cache` module. We can use this utility to invalidate the cache of the `/user` route when the user information is edited. This is what the updated `actions.js` file looks like this:

```javascript
try {
  await kv.hset('user:123', { id: 123, email: 'test@me.com', name: 'Test User' });
  revalidatePath('/user')
} catch (error) {
  console.log('error while editing user')
}
```

By calling the `revalidatePath` function, we are telling Next.js that modifying the user information in the DB should invalidate the cache of the `/user` route and that Next.js should re-render the `/user` route with the updated information. 

Now, when we click the **Edit User** button, the email of the user changes to `test@me.com` and it also gets updated on the `/user` page. Here's a GIF that shows the edit action and the user page: ![Testing User](https://blog.logrocket.com/wp-content/uploads/2024/05/testing-user-gif.gif) But there's a small caveat with this approach. Because we modified a small detail for a user, we invalidated the cache for the whole `/user` route. In real-world applications, it might be the case that the route shows us lots of information and the user information might just be a small part of it. In such cases, we would prefer a more granular control over the cache invalidation.

### Using `unstable_cache` for granular control

This is where the `unstable_cache` API can be useful. We will first cache the result of our call to the DB inside our `page.ts` in the `/user` route. This is what the updated code looks like:

```javascript
import { unstable_cache } from 'next/cache';

export function getUser() {
  return kv.hgetall('user:123');
}

const getCachedUser = unstable_cache(
  async () => getUser(),
  ['my-app-user'],
  { tags: ['user'] }
);

export default async function User() {
  const user = await getCachedUser();
  // rest of the component
}
```

Notice the `unstable_cache` function that takes our original `getUser` function as an argument. We have dual benefits from using this function:

*   Firstly, we have cached the result of the call to the DB. This means that the call to the DB will only happen once and the result will be cached for subsequent calls
*   Secondly, we are also passing the `tags` option with the value `['user']`. This is important because we can use the `tags` option to have granular control over the cache invalidation

To take advantage of this granular control, we just have to change the `revalidatePath(/user)` to `revalidateTag('user')` in the `actions.js` file. And that's it! To test out the code, let’s change the email to `testing@me.com` this time:

```javascript
await kv.hset('user:123', { id: 123, email: 'testing@me.com', name: 'Test User' });
```

Here's what the updated behavior looks like: ![Updated User Button Action](https://blog.logrocket.com/wp-content/uploads/2024/05/user-button.gif) It looks the same as the previous example but this time we have a more granular control over the cache invalidation. We can now invalidate the cache of only the API and not the entire `/user` route.

## React `cache` API

At this point, we also need to talk about a React function that is similar to the `unstable_cache` function. This function is called the `cache` function and it is exported from the `react` module. This is what the code looks like:

```javascript
import {cache} from 'react';
import calculateReport from 'lib/reports';

export const getReport = cache(calculateReport);

function Chart({data}) {
  const report = getReport(data);
  // ...
}
```

As we can see, this takes just one argument, which is the function we want to cache. When we want to cache an API call, that function will be an async function that wraps that API call. While calling the function, we need to pass the arguments that we would normally pass to the uncached function. If the cached function is called with the same argument again, the cached result will be returned. 

While this can be useful, we would prefer to use the Next.js cache API because of how seamlessly it integrates with the invalidation strategies that Next.js provides. 

For example, unlike before, there is no way to link the cached function with tags and then use it for invalidation. 

The React cache function is best utilized for caching expensive calculations inside client components where invalidation is not a concern. Another good use case for the React cache function is when you want to "prefetch" some data. 

Let’s say that we have a parent component similar to the `Chart` component that calls the cached `getReport` function but does not use its result. In that case, it might look like this:

```javascript
function Chart({id}) {
  getReport(id);
  return (
   <>
     <MiniChart id={id} />
   </>
  );
}
```

Then, the `MiniChart` function is the one that actually uses the data returned by the `getReport` function:

```javascript
import getReport from './Chart;

function MiniChart({id}) {
  const report = getReport(id);
  return (
   <>
     // use the report here
   </>
  );
}
```

In this way, the parent pre-fetches the data even though it does not use any of it. However, the data is more likely to be available by the time the child component gets to use it.

## Conclusion

In this article, we learned about the different caching strategies in Next.js. We explored the `unstable_cache` function and learned how it can be used to cache the result of DB queries that do not involve the `fetch` API. The use of `tags` also offered granular control over cache invalidation, enhancing efficiency. 

It is great to see that Next.js finally has a low-level cache API that can be used to cache the results of expensive operations. Developers now have a choice between this and higher-level caching methods, or integrating the React `cache` function, based on their specific needs.

---

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

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

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

[LogRocket](https://lp.logrocket.com/blg/nextjs-signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js 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://lp.logrocket.com/blg/nextjs-signup).