ProNextJS
    Professional Next.js Course
    Loading price
    30-Day Money-Back Guarantee
    lesson

    Cache-busting with Tags

    Jack HerringtonJack Herrington

    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:

    route is dynamic

    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:

    the db-time route

    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.

    Transcript

    Let's say instead that you're dealing with a database. So let's go create a new example called dbtime. And before I make the page, I'm gonna create our database. So I'm gonna call it dbtime.ts. And in there, I'm simply gonna have a function that returns time.

    It could go off to a database and get time, or it could just do this. Honestly, this is fine. So let's bring that into our page, and then we'll create a simple RSC around it that gets that DB time, gives ourselves a nice little console log, and then puts that out for us. Okay, let's give it a try. Okay, so what would you expect?

    Is this going to be a static page or a dynamic page? Let's go to have a look. So if we take a look, we can see that time, the API endpoint is dynamic, but our DB time is static because it hasn't detected anything in our DB time route that is dynamic so therefore it will be static. So let's go back over into our slash DB time and there we go we've got our time for the DB and it's static and it's not changing. Now, of course, we could just go and go into our route and say that we want it to be dynamic.

    By forcing it to be dynamic, that would make the whole page and any fetches and any database calls completely dynamic. That works. Let's try that out. Now, even in problem we get a new time with each refresh. But what if we want to be more surgical?

    What if we want to actually go and bring it into our Next.js data cache because that's doing some good stuff for us. So we can use is something called unstable cache. So bring in unstable cache from next cache. This is the Next.js cache, not to be confused with the React cache. React cache is a cache that is only available on a per request basis.

    Unstable cache is a persistent cache that goes between requests. Next step, we'll rename this to GitDBTimeReel and we won't export it, but instead we'll export a replacement called GitDBTime which wraps unstable cache around that get DB time real. All right, so let's try that. Now, let's hit refresh. As we can see, it's locked even though, check this out.

    So go back to my page.tsx. This is a dynamic route. We've actually forced it to be dynamic and it is still caching. So why is that? Well, yeah, it's a dynamic route so that unstable cache has actually stabilized that data between each request, even though the route is dynamic.

    All right. So can we go and bust that cache? Let's go create a button so we can bust the cache. We'll call it Revalidate DB Time Button. It's gonna be the same kind of thing, takes a Revalidate Server Action and wraps a button around it.

    So let's bring that into our code and use it. So what would the server action do? Well it needs to revalidate something. So we don't want to revalidate the path because we are already dynamic. That's not going to help.

    What we can do is we can revalidate a tag and then assign that unstable cache to that tag. So let's bring in revalidate tag and give ourselves a server action where we will revalidate the dbtime tag if we get called. Let's give that a try. All right, let's go over to browser, hit refresh. Now we've got to revalidate DB time.

    Let's hit that and see that's not happening. So what's the deal? We haven't actually connected this DB time tag with our unstable cache. So is it this? Because dbTime takes an additional parameter.

    So is that the right thing to do? Let's give it a try. All right, we'll hit refresh, revalidate DB time and nope, that's not happening. So what is that second parameter? Well, that second parameter is actually just the cache name, just helping Next.js identify which cache is which internally.

    If we want to add tags, we add an optional extra parameter called and then inside of a tags key then we give it our tag. Okay let's give that a try. All right let's hit refresh and there we go Now we have our tag updating behavior. Now of course if you wanted to you could still use the caching by getting rid of dynamic and exposing the real DB time and using that instead and then instead of revalidating the tag we will revalidate the path of slash DB time. Let's see if that all works.

    Now we can see that dbtime is a static path. All right, let's see. Hit refresh and it's stable and then revalidate DB time. Now, we are cached again at that point. So those are different options when it comes to caching and using the data cache and the full route cache together really depends on the needs of your application, which you want to use.