ProNextJS
    Loading
    lesson

    Fetch Caching Behavior

    Jack HerringtonJack Herrington

    Fetching Data Dynamically with Next.js

    let's explore how to fetch data dynamically in a Next.js application, ensuring that the fetched data is never cached. We'll be working with a small Fastify time service that returns the current date.

    The time service is a simple Fastify application that listens for requests on localhost:8080. When it receives a request at the root path (/), it returns the current date. Here's the Fastify code:

    import Fastify from "fastify";
    
    const fastify = Fastify({
    	logger: true,
    });
    
    fastify.get("/", async function handler() {
    	return { date: new Date().toLocaleString() };
    });
    
    try {
    	await fastify.listen({ port: 8080 });
    } catch (err) {
    	fastify.log.error(err);
    	process.exit(1);
    }
    

    To run the time service, open a new terminal, navigate to the time service folder, install the dependencies, and start the server:

    pnpm install
    node server.mjs
    

    Now, if you visit localhost:8080 in your browser, you'll see the current date.

    Fetching Data in the Next.js App

    In our Next.js app, we want to fetch the date dynamically from the time service. To do this, we'll use the fetch function and wrap the fetched data in a React Suspense component. This tells Next.js that the data should never be cached and should always be fetched dynamically.

    Here's the implementation of the FetchDynamic component:

    import { Suspense } from "react";
    
    async function Date() {
    	const dateReq = await fetch("http://localhost:8080/");
    	const { date } = await dateReq.json();
    
    	return <div>Date from fetch: {date}</div>;
    }
    
    export default async function FetchDynamic() {
    	return (
    		<div className="flex gap-5">
    			<Suspense fallback={<div>Loading...</div>}>
    				<Date />
    			</Suspense>
    		</div>
    	);
    }
    

    Now, when you visit the Dynamic Page in your Next.js app, you'll see the current date fetched dynamically from the time service without caching.

    The value of using a suspense is if that localhost 8080 request takes a while, then we'll automatically get that loading state in there.

    Let's give it a try and see if it works. We'll go to server.mjs and add a delay of three seconds to the server.

    await new Promise((resolve) => setTimeout(resolve, 3000));
    

    After rebooting the server and refreshing the page, we get a loading state, and then three seconds later, our data appears. This is really cool because you automatically get that behavior, and you can define what the loading state UI looks like.

    Now, let's add a cache to our data for just a few seconds. In Next.js, we would usually use revalidate to specify how often we want to revalidate the data. However, we're not going to do that in this example.

    Instead, we'll bring in our "use cache" and give it a cacheLife.

    "use cache";
    cacheLife("seconds");
    

    We're using seconds here, which is a profile. There are several built-in profiles that come with cacheLife. It's totally up to you, and you can define your own profiles if you want.

    Cache Seconds and Suspense

    When you click on cache seconds, you might still get an error. The error says that you need to wrap it in a suspense boundary. This is because you're either accessing dynamic data or you have a short-lived cache.

    When you have a cache that's only around for seconds, you still need to wrap it in a suspense. To fix this, bring in the suspense component and import it from React.

    After adding the suspense component, the seconds cache should work as expected.

    Cache Minutes

    Now, let's take a look at caching for minutes. In this case, the revalidate value is set to 60 seconds, which is a minute.

    async function Date() {
        const dateReq = await fetch("http://localhost:8080/", {
            cache: "force-cache",
            next: {
                revalidate: 60,
            },
        });
    

    To cache for minutes, you can use unstable_cacheLife and set the cache life to minutes. Refresh the page and navigate to the cache for minutes section. You can keep refreshing, and if it's over a minute since the last time you refreshed, you'll get an update. Since this is not a short-lived cache, you don't have to wrap it in a suspense.

    "use cache";
    cacheLife("minutes");
    

    Fetch Caching Using a Tag

    Let's try out fetch caching using a tag. We've got an invalidate button that lets us invalidate the tag, and it looks like it's working. The reason it works is because in dynamicIO mode, it still supports this behavior.

    So, we're going to do the following:

    1. Use Use Cache and say that we're caching on a tag.
    2. Bring in unstable_cache_tag as cache_tag and expire_tag so we can expire it.
    3. Change our revalidate function to give us the tag and fetch date.

    You can see that only when we invalidate do we make the fetch. How do we know that? If we go back to our terminal, you can see that we can hit the route as many times as we want without hitting the API endpoint. When we hit Invalidate, we see the API endpoint getting a request from the server, which means that we've invalidated the cache, so we're going to fetch the data again.

    It's essential to understand that architecturally, you can decide the cache behavior using these cache semantics. Each section of data on a page may have different semantics for caching.

    Caching Strategies for Different Data Types

    In some cases, you might want to use different caching strategies for various types of data within your application. For example:

    • Product Information: This data may not change frequently, so you can cache it for a longer duration, say hours.
    • Product Price: As prices can change more frequently, you might want to cache them for a shorter period, such as seconds.
    • Product Comments: These can be cached using a tag-based approach. When a new comment is added, you can invalidate the tag associated with the comments and fetch the updated comments. This way, you don't need to hit the back-end service to get the comments unless there's a change in the comments.

    This gives you all the power tools you need to manage caching however you want.

    Transcript

    All right. So back in our example code, we're going to go down to our Fetch section. I'm going to grab all of the Fetch examples and I'm going to copy them and then paste them up into the app. So we'll start off with Dynamic. What does Dynamic look like?

    So first off, Our goal here is to never catch the Fesh date. So where are we even getting the date? Well, we're getting on off of localhost 8080. So there's a little time service down here. Time service, let's take a look at that.

    We've got a Fastify time service here. We create our Fastify app, we give it, say that we want some logging. Then anytime we get slash, we return the date, and that's it. Love Fastify. So let's go and create another terminal.

    We'll go into that time service and we'll run it. To do that, we install and then we do node server.mjs, and There we go. Now, if I were to go over to our Arc, hit localhost 8080, we get the date. Super easy. Let's go back to our app.

    Now, we want to do a Fetch Dynamic and we can start to see, we're getting some errors. The error here, Fetch Dynamic, we're doing something dynamic and we haven't specified whether we want to use a suspense boundary or use cache. So let's go take a look at our implementation. So fetch dynamic, go into the page. So, we never want to cache it, we always want to get it dynamically.

    So, what do we do? Well, we use a suspense. We simply wrap our date component in a suspense that tells Next.js 15 that has dynamic data. And then up at the top there, we've imported suspense. Let's take a look.

    And there it is. No cache on the fetch. It's that easy. It's really great. The value of using a suspense is if that localhost 8080 request takes a while, then we'll automatically get that loading state in there.

    Let's actually give that a try and see if it works. So go over here to Time Service, go to the server. We'll await a new promise for a three second delay. We'll reboot that server. And now if I hit refresh, then we get a loading state.

    This is great. And three seconds later, we get our data and that's it. That was so cool. That's really the value of using suspense for this caching stuff, is you automatically get that behavior. You get to define what those skeletons, I'm using loading in this one, but you can get to define whatever that UI looks like for that wait state.

    This is really nice. Let's go and take out that wait state. Then we'll go to our next example, which is to cache it for some amount of seconds, just a few seconds, that's all. All we want is cache, just for a few seconds. So let's go take a look at that.

    So we'll go over here. Now normally, back in Next.js 15, we would say that our cache behavior is to force the cache, and then we give a special next directive to say that we want to revalidate it every three seconds or so. So we're not gonna do any of that. We're just gonna get rid of that, cool. And then we're gonna bring in our useCache.

    And then we're gonna give it a cacheLife. So we say cacheLife, which we bring in as unstableCacheLife, and then we give it seconds. Now we're gonna use seconds here. Now seconds is a profile. There's a bunch of built-in profiles that come with cache life.

    You get it fine whether you want to cache for seconds or minutes totally up to you and you can define your own profiles if you want. Okay, let's give it a go. So I click on cache seconds, I still get an error and that error says that we still need to wrap it in a suspense boundary and here is the trick. So what's happening here is it's saying that you're either accessing dynamic data or you have a short-lived cache. So when you have a cache that's only around for seconds you still need to wrap it in a suspense.

    So let's bring in that suspense and then bring in that suspense from React. Looks good. Hit refresh, and now we get that seconds cache. Let's take a look at the next example, and that's cached for minutes. Let's take a look.

    So if I go over here to fetch cached minutes, you can see that we were doing exactly what we're doing for it. The revalidate at this point is 60 seconds, so a minute. So let's get rid of that, and I'll bring in unstable cache life. I'll say that this is cached and the cache life is minutes. Hit refresh.

    So we'll go over to your cache for minutes and yes we can keep refreshing And if it's over a minute since the last time we refreshed, then we'll get an update. So this is not a short-lived cache, and therefore we don't have to wrap it in a suspense. All right, one last one. Let's try out fetch caching using a tag. So we've got an invalidate button that allows us to invalidate that tag.

    Yeah, it looks like it's working, but let's go take a look and see how to update it. The reason that it is working is because in this Dynamic IO mode, It still supports all of this behavior. So you can still use the fetch caching behavior as is, but I don't think you really should. I think you should, if you're using this dynamic IO, use the dynamic IO method. So we're going to give it useCache.

    We're going to say that we're caching on a tag. So we're going to bring in unstable cache tag as cache tag and then expire tag so we can expire it. Let's change out our revalidate and that's gonna give us our tag fetch date and there we go hit save and there we go looks good and we can see that only when we invalidate do we make the fetch how do we know that because we go back over here to our terminal you can see let's clear that out and I can see that I can hit this route as many times as I want. I don't hit the API endpoint, and then when I hit Invalidate, I do see the API endpoint getting the request from the server, which means that we've invalidated that cache and we're going to do that fetch again. Now a really important part to realize about this is architecturally you can decide using this cache behavior all of the cache semantics you have on the page.

    Each section of data on a page may have different semantics for caching. If you have a product detail page, for example, the product information may be on a longer cache, say hours, whereas the price of the product may be on a seconds cache. You want to always go and get the updated price, whereas the comments for the product might be on a tag so that when a new comment is added, we invalidate that tag and we go get the comments. But otherwise, all of those comments are cached because we don't want to hit that backend service to go get those comments unless the comments have changed. So This is giving you all the power tools that you need to manage the caching however you