There are different ways to fetch data from a database and cache that data in Next.js. To make things interesting, we'll simulate database interactions and observe how caching behaves with dynamic routes.
We'll start by creating a new db-time
route and page at app/db-time/page.tsx
. This page will fetch the current time from our simulated database, but before we build the page we'll create the database itself in a file db-time.ts
:
// inside app/db-time/db-time.ts
export async function getDBTime() {
return {
time: new Date().toLocaleTimeString();
};
}
Then for the page, we'll import the getDBTime
function and render it:
// inside app/db-time/page.tsx
import { getDBTime } from "./db-time";
export default async function DBTime() {
const { time } = await getDBTime();
console.log(`Render /db-time ${new Date().toLocaleTimeString()}`);
return (
<div>
<h1 className="text-2xl">Time From DB</h1>
<p className="text-xl">{time}</p>
</div>
);
}
We can see that the time
route is dynamic, but our db-time
route is static because it hasn't detected anything dynamic in the route:
If we visit the /db-time
route, we'll see that the time is static and doesn't change on each refresh.
Forcing Dynamic Rendering
As we've seen, we could use the force-dynamic
option to make the route dynamic, but this would make the entire page dynamic.
In order to be more surgical and specific with our caching, we can use Next.js's unstable_cache
, which is a persistent cache that goes between requests:
// inside app/db-time/db-time.ts
import { unstable_cache } from "next/cache";
export async function getDBTimeReal() {
return { time: new Date().toLocaleTimeString() };
}
export const getDBTime = unstable_cache(getDBTimeReal);
Now the db-time
route is dynamic:
However, the cache is still active between requests, even though the route is dynamic.
Adding a Revalidation Button
Similar to before, we'll add a button to revalidate the cache:
// inside app/db-time/RerenderDBTimeButton.tsx
"use client";
import { Button } from "@/components/ui/button";
export default function RevalidateDBTimeButton({
onRevalidate,
}: {
onRevalidate: () => Promise<void>;
}) {
return (
<Button onClick={async () => await onRevalidate()} className="mt-4">
Revalidate DB Time
</Button>
);
}
Back in the page.tsx
file, we'll being in the button and use the revalidateTag
function to revalidate the cache:
import { revalidateTag } from "next/cache";
import { getDBTime } from "./db-time";
import RevalidateDBTimeButton from "./RevalidateDBTimeButton";
export const dynamic = "force-dynamic";
export default async function DBTime() {
const { time } = await getDBTimeReal();
console.log(`Render /db-time ${new Date().toLocaleTimeString()}`);
async function onRevalidate() {
"use server";
revalidateTag("db-time");
}
return (
<div>
<h1 className="text-2xl">Time From DB</h1>
<p className="text-xl">{time}</p>
<RevalidateDBTimeButton onRevalidate={onRevalidate} />
</div>
);
}
Then back in the db-time.ts
file, we'll add the tag to the call to unstable_cache
:
// inside app/db-time/db-time.ts
export const getDBTime = unstable_cache(getDBTimeReal, ["db-time"], {
tags: ["db-time"],
});
Clicking this button will trigger the server action, which invalidates the 'db-time'
tag, causing Next.js to re-fetch data on the next request.
As we've seen, using revalidatePath
is also an option, but using tags allows for more granular control over which cache to invalidate.