Fetch Caching Behavior
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:
- Use Use Cache and say that we're caching on a tag.
- Bring in unstable_cache_tag as cache_tag and expire_tag so we can expire it.
- 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.