DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on

Data cache in NextJs

Until now, we've only worked with "hard coded" components, no external APIs. Why? Because of Next data cache.

Data cache allows the result of external requests (using fetch) to be stored on the server. It also handles revalidation of this data. Data cache means that instead of making a request twice, the second request would just return the cached result of the first one. Of course, you can always opt out and force a fresh request every time.

data cache and full route cache are intertwined. They alter each others behavior, and it can become tricky to know what's happening or who's doing what. The goal of this chapter is to clear the fog.

Cache options

Next extends the native fetch api with options. One of these options is cache:

fetch('url', { cache: '...' });
Enter fullscreen mode Exit fullscreen mode

There are 3 options:

  1. {} (auto option) -> does not cache
  2. { cache: "no-store" } -> does not cache
  3. { cache: "force-cache" } -> caches

From before, we know that a route becomes dynamically rendered when Next encounters a dynamic element: cookies, headers, collection, draftmode, searchParams page prop or the dynamic fetch: { cache: "no-store" }.

So, out of our 3 options above, the dynamic fetch: { cache: "no-store" }, will make the route dynamic. The other 2 will not.

1. {} auto

fetch('url');
Enter fullscreen mode Exit fullscreen mode

Omitting the cache property is called the auto option. By default, the fetch result will not be cached in the data cache. The auto option does not opt the route into dynamic rendering.

  • auto option + static route: Next will - at build time - make the fetch, render the result into HTML and rsc. The route is cached, the result is not cached (no data cache). Next serves the HTML and rsc but never makes the fetch again.
  • auto option + dynamic route: Next will - at request time - make the fetch, render the result into HTML and rsc but not cache the fetch result. For each request, the fetch will be made and the route will be rendered (into HTML and rsc).

This is as expected. The auto option does not interfere with static or dynamic rendering, they behave normally. When a route is static, it remains static, when a route is dynamic it remains dynamic. In a static route, the fetch is made only once at build time. In a dynamic route, the fetch is made for each request, at request time. The fetch result is never cached.

2. { cache: "no-store" }

Setting a fetch with no-store in Next is called a dynamic fetch. Data will never be cached, we always want fresh data. You can also use no-cache. It has the same result in Next.

A dynamic fetch is a dynamic element. This means that it turns your route into dynamic rendering: server-side at request time. So, when using no-store a route can never be static: no prerendered HTML or rsc at build time. The route is skipped and only rendered at request time. So, opting out of data cache, also opts you out of the full route cache.

This makes sense, when you always want fresh data, you cannot prerender it in advance, only "now", at request time.

3. { cache: "force-cache" }

Setting cache to force-cache means that Next will use the cache as much as possible. When using force-cache, Next will first check the data cache for a matching request. If there is none or if the match is stale, a new fetch will be made and cached.

force-cache does not force a route into dynamic rendering.

  • A static route with force-cache will remain static. At build time Next uses the cache or makes a new fetch and caches it. It then prerenders the HTML and rsc.
  • A dynamic route with force-cache will remain dynamic. At request time Next uses the cache or makes a new fetch and caches it. It will render HTML and rsc but it won't cache it.

Recap

  • auto and no-store do not cache fetch results, whereas force-cache does.
  • no-store forces a route into dynamic rendering. auto and force-cache do not.

Testing

Let's test this out and see how it looks. Note: the examples are available on github. We created a <Post > component.

// components/Post.tsx

type Props = {
  postId: number;
  fetchOptions: RequestInit;
};

export type PostT = {
  id: number;
  title: string;
};

export default async function Post({ postId, fetchOptions }: Props) {
  const data = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`,
    fetchOptions,
  );
  const post: PostT = await data.json();
  // get date from headers
  const fetchDate = data.headers.get('date');
  const time = fetchDate
    ? new Date(fetchDate).toTimeString().slice(0, 8)
    : null;
  return (
    <div className='flex gap-2'>
      <div className='text-orange-400 mr-2'>{time}</div>
      <div className='font-bold'>{post.id}.</div>
      <div className='italic'>{post.title}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This component:

  • Takes an id and fetchOptions ({ cache: ... }) as arguments
  • Makes a fetch to JSONPlaceholder posts.
  • Renders the date, id and title.

Note that we get the date from fetch response headers. This is the date the fetch was made. We'll be using this later on in the chapters on revalidation.

We create a static route and load a <Post /> with auto option and one with "force cache".

// app/cache/static/page.tsx

import Post from '@/components/Post';

export default function Page() {
  return (
    <div>
      <h2 className='font-bold mb-4'>Static route + cache options</h2>
      <Post postId={10} fetchOptions={{}} />
      <Post postId={11} fetchOptions={{ cache: 'force-cache' }} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We create another route:

// app/cache/notstatic/page.tsx

import Post from '@/components/Post';

export default function Page() {
  return (
    <div>
      <h2 className='font-bold mb-4'>
        Dynamic element in static route + cache options
      </h2>
      <Post postId={12} fetchOptions={{ cache: 'no-store' }} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Did you catch it? A fetch with "no-store" is a dynamic fetch, forcing the route into dynamic rendering. We also want to test static rendering so we had to put the dynamic fetch (no-store) in a separate route.

Finally, a dynamic route:

// app/cache/dynamic/page.tsx

import Post from '@/components/Post';

export const dynamic = 'force-dynamic';

export default function Page() {
  return (
    <div>
      <h2 className='font-bold mb-4'>Dynamic route + cache options</h2>
      <Post postId={13} fetchOptions={{}} />
      <Post postId={14} fetchOptions={{ cache: 'no-store' }} />
      <Post postId={15} fetchOptions={{ cache: 'force-cache' }} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Again, the dynamic fetch will force the route into dynamic rendering so the route segment option dynamic is not necessary but we just wanted to make it explicit. It's fine.

For a better overview, here's a schematic of the routes we build:

/static route
  - fetch 10: auto
  - fetch 11: force-cache
/notstatic route
  - fetch 12: no-store
/dynamic route
  - fetch 13: auto
  - fetch 14: no-store
  - fetch 15: force-cache
Enter fullscreen mode Exit fullscreen mode

So, what do we expect here when we run next build?

  1. The static route should have been statically rendered into HTML and rsc and be in the full route cache. The notstatic and dynamic routes should not.
  2. Fetch with auto cache should not be in data cache: posts 10 and 13 should not be in cache.
  3. Fetch with "no-store" should not be in data cache: posts 12 and 14 should not be in cache.
  4. Fetch with "force-cache" should be in data cache: posts 11 and 15 should be in cache.

Two notes here:

  • We're using different post ids here so other routes don't pollute the results.
  • Sometimes, depending on the environment, data cache persists between builds. To avoid this, I delete the .next folder before each build.
next build
Enter fullscreen mode Exit fullscreen mode

The build log confirms the route expectations:

├ ƒ /cache/dynamic
├ ƒ /cache/notstatic
├ ○ /cache/static

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
Enter fullscreen mode Exit fullscreen mode

Also, In the .next/server/app/cache folder: static.html and static.rsc exist. The other routes are absent. What we confirmed here is that dynamic fetches do force routes into dynamic rendering. Great, on to the data cache.

Generated data cache files

A quick word about the actual data cache files. These are on the server (data cache is always on the server) in the build folder: .next/cache/fetch-cache. Each cached fetch has it's own file (hash name with no extension).

This is how the data cache entry for post 11 ("force-cache") looks like:

data cache file

I made some markings, it contains:

  • Entire response object of the fetch: headers, status, url, and the serialized body.
  • Metadata from Next.js: revalidate and tags.

Don't worry about this, you don't have to know or understand this. Just remember, the entire response is saved and some Next meta data are saved. We can't read the actual data because they are serialized.

Note: I was also wondering how Next finds a match inside this cache but it's complicated and in the end it doesn't matter. Next knows how to do this and how to check if the data isn't stale. I'm leaving it at that.

Testing continued

We confirmed that static and dynamic rendering happened as expected. Now, we need to check the data cache.

/static route
  - fetch 10: auto
  - fetch 11: force-cache
/notstatic route
  - fetch 12: no-store
/dynamic route
  - fetch 13: auto
  - fetch 14: no-store
  - fetch 15: force-cache
Enter fullscreen mode Exit fullscreen mode

We found posts 10 and 11 in the data cache, nothing else. This is unexpected. What happened?

  • The "no-store" fetches behaved as expected. 12 and 14 are not cached.
  • We did not expect the auto fetches (10 and 13) to be cached. But 10 was. Weird.
  • We expected the "force-cache" fetches 11 and 15 to have been cached. Only 11 was. 15 is missing.

We tackle this last one first. 15 is missing. Why? Because we didn't think this through. We only ran next build. But we have a dynamic route. Dynamic routes don't get rendered at build time, only at request time. We haven't made a request for the dynamic route so fetches 13-15 haven't been made yet.

next start
Enter fullscreen mode Exit fullscreen mode

We visit our 3 test routes: /cache/static, cache/notstatic and /cache/dynamic and revisit the data cache folder. One new file was added: post 15 for the "force-cache" fetch in the dynamic route. This is as expected. We expect fetch 15 with "force-cache" to have been cached. Great, first problem fixed.

The "Auto" Mystery

The second problem. Fetch 10 with auto caching should not have been cached but it was. Why? Note, we ran the dynamic route. This route also has an auto fetch (13). However, this auto fetch was not added to the cache. So 13 behaved normally and this seems to be a problem with static rendering.

I couldn't figure it out, so I asked AI. This is how Gemini explained it:

In Next.js, during the build process (Static Site Generation), fetch requests are actually cached by default even if you use the auto setting. This is because Next.js needs to ensure that the static HTML it creates has data to show. If it didn't cache that "auto" fetch during the build, the static page would have nothing to display! This is why post 10 appeared in your folder, while post 13 (the dynamic one) did not.

Seems plausible but I can't verify this is correct. This sorta insinuates that during a render, fetches are made first and then the actual render is made. Maybe.

In any case, it doesn't really matter that much. In static rendering, the API is hit once and then the route is rendered into HTML and rsc. Only this full route cache is served. The endpoint isn't hit again (unless you revalidate). We will leave it at this.

Summary

We learned how to use data cache in Next using the cache option on the fetch API. We also saw how using a dynamic fetch (no-store) forces the entire route into dynamic rendering and took a quick peek at the data cache files. Lastly we ran some tests that - mostly - confirmed the theory.

This should give you a more grounded feel for the data cache mechanism in Next. In the following chapter, we look into revalidation.

If you want to support my writing, you can donate with paypal.

Top comments (0)