ProNextJS
    Loading
    lesson

    The Next.js Router Cache

    Jack HerringtonJack Herrington

    The Next.js App Router features a built-in client-side cache, known as the router cache.

    The router cache is designed to make navigation between routes super efficient. When you visit a route in your Next.js app, the page content gets stored in the cache. If you revisit that same route later, the cached version will load instantly, resulting in a much smoother user experience.

    The Example Application

    Inside of router-cache-test from the repo is an example App Router app that shows the current time on the homepage. The time updates every second, giving the impression of a live clock.

    To test the cache, we'll try navigating from one route to another. First, we need to create a sub-route at app/sub-route/page.tsx that links back to the homepage:

    // inside app/sub-route/page.tsx
    
    import Link from "next/link";
    
    export default function SubRoute() {
      return (
        <main className="flex flex-col gap-3">
          <Link href="/">Home</Link>
        </main>
      );
    }
    

    Then in the main page.tsx file, we'll ad a link to the sub-route:

    import Link from "next/link";
    import Timer from "./Timer";
    
    export default function Home() {
      return (
        <main>
          <div>Time: {new Date().toLocaleTimeString()}</div>
          <div>
            <Link href="/sub-route">Sub-Route</Link>
          </div>
          <Timer />
        </main>
      );
    }
    

    If we build and run the app with pnpm build && pnpm start, the homepage no longer updates the timer.

    Examining the build output, we'll notice that both the homepage (/) and the sub-route (/sub-route) are marked as statically generated. This means they are pre-rendered at build time and served as static HTML files:

    static routes

    What we're seeing is the expected behavior of SSG, not a router cache issue. The caching is in the browser, not in the router.

    Forcing Revalidation with Server Actions

    In order to bust the cache in the browser, we need to use the router hook to refresh the page.

    Inside of the sub-route/page.tsx file, we can bring in the useRouter hook from next/navigation and create a button that calls router.refresh():

    // inside app/sub-route/page.tsx
    "use client";
    
    import Link from "next/link";
    import { useRouter } from "next/navigation"
    
    import { Button } from "@/components/ui/button";
    
    export default function SubRoute() {
      const router = useRouter();
    
      return (
        <main className="flex flex-col gap-3">
          <Link href="/">Home</Link>
          <div>
            <Button onClick={() => router.refresh()}>Bust The Cache</Button>
          </div>
        </main>
      );
    }
    

    After we build and run the app again, we can navigate to the sub-route, click the "Bust The Cache" button, and then return to the homepage. However, the time is still stuck.

    Again, this is not a router cache issue. The homepage route is static and won't rebuild unless it is revalidated.

    Revalidating the Homepage

    The way to fix this is with a server action. Create a new file revalidate-home.tsx that revalidates the / route:

    // inside app/revalidate-home.tsx
    "use server";
    import { revalidatePath } from "next/cache";
    
    export async function revalidateHome() {
      revalidatePath("/");
    }
    

    We can then call this server action from our sub-route component, right before navigating back to the homepage:

    // inside app/sub-route/page.tsx
    "use client";
    import { useRouter } from "next/navigation";
    
    import { Button } from "@/components/ui/button";
    
    import { revalidateHome } from "../revalidate-home";
    
    export default function SubRoute() {
      const router = useRouter();
    
      return (
        <main className="flex flex-col gap-3">
          <div>
            <Button
              onClick={async () => {
                await revalidateHome();
              }}
            >
              Go Home
            </Button>
          </div>
        </main>
      );
    }
    

    Now, whenever we navigate back to the homepage from the sub-route, the server action will revalidate the route, ensuring that the timer is updated with the latest time!

    It's also possible to add functionality that will revalidate the cache and then navigate back to the homepage. We can do this by calling router.push("/") after revalidating the homepage:

    // inside app/sub-route/page.tsx
    
      return (
        <main className="flex flex-col gap-3">
          <div>
            <Button
              onClick={async () => {
                await revalidateHome();
                router.push('/');
              }}
            >
              Go Home
            </Button>
          </div>
        </main>
      );
    

    The Role of Response Headers in Revalidation

    When a server action is executed, the Next.js client sends a request to the server with a special next-action-id header. The server recognizes this header and executes the corresponding server action. If this action includes revalidatePath or revalidateTag, the server's response will include a x-nextjs-revalidate header. This header tells the Next.js client to invalidate its cache for the specified route or tags, which will trigger a revalidation on the next navigation.

    Handling Revalidation without Server Actions

    What happens if we want to revalidate a route without directly calling a server action? For example, let's say we have an API route that modifies data used on the homepage.

    Let's create an API route called revalidateHome at app/api/revalidateHome/route.ts that revalidates the homepage:

    import { revalidatePath } from "next/cache";
    import { NextResponse } from "next/server";
    
    export function POST() {
      revalidatePath("/");
      return NextResponse.json({ message: "Home invalidated" });
    }
    

    We can then post to the API route from the sub-route component instead of calling the server action:

    <Button
      onClick={async () => {
        await fetch("/api/revalidateHome", { method: "POST" });
        // await revalidateHome();
        router.push("/");
        router.refresh();
      }}
    >
      Go Home
    </Button>
    

    On-Demand Page Updates with router.refresh()

    We can also use router.refresh() to update the current page content on demand, without navigating to a different route. This is useful when we want to refresh the page after performing an action that might change the data being displayed.

    Let's see how we can make our homepage dynamic and update it periodically using router.refresh().

    First, we'll force the homepage to be dynamic by exporting a constant called dynamic with the value "force-dynamic":

    // inside app/page.tsx
    
    // code as before
    
    export const dynamic = "force-dynamic";
    

    Then inside of the Timer component, add a useEffect that will call router.refresh() every second:

    // inside src/app/Timer.tsx
    
    "use client";
    import { useEffect } from "react";
    import { useRouter } from "next/navigation";
    
    export default function Timer() {
      const router = useRouter();
    
      useEffect(() => {
        const timer = setInterval(() => {
          router.refresh();
        }, 1000);
        return () => {
          clearInterval(timer);
        };
      }, [router]);
    
      return null;
    }
    

    Now, the homepage will refresh every second, requesting new content from the server, then merging it into the DOM to show the current time without flickering or needing to navigate away.

    When to Worry About the Router Cache

    The good news is that the router cache usually works seamlessly in the background. You'll typically only need to manually interact with it when working with API routes that modify data used on your pages or when you need fine-grained control over cache invalidation.

    Transcript

    The Next.js App Router also has a cache on the client. It's called the router cache. And the idea of the router cache is that when you're navigating from route to route, that makes it really efficient. So if you jump back to a route where you've already been, it's already cache and you're already going to see the same page. Now normally, this isn't the kind of thing that trips folks up, but I have seen a few folks run into some issues with it.

    So let me just kind of walk you through it and show you where some of the sticking points are and then show you how to get past them. So This is an example application that I put together. All I've done is create a new Next.js App Router app. I have selected Tailwind as a styling, installed ShadCDN, and installed a button just to make it look nice. And here's what it looks like in the browser.

    I can refresh. I'm in dev mode and I just keep seeing that timer go up and up and up. All right. So let's try out navigating from one route to another. To do that, I'll create a subroute first, subroute page.tsx.

    I'll bring in a simple implementation that just brings in link and then a link to home. So I'll go back into our page, and then I'll give us a link to the sub-route. All right, let's have a look. Now, because we're in dev mode, of course, we get hot module reloading and that looks really good. But what if we run this in build mode?

    Let's give that a try. So I'm gonna do pmpmbuild and then pmpmstart. All right, I'm on 09 there. I go to my subroute and I come back to home and I would expect maybe that that time would get updated, but it's not updating. And some folks look at that and say, well, that must be the router cache.

    But it is in reality is the route cache because we look back on the build we can see that both slash and subroute are both identified with an O which means that they're both static and pre-rendered so you can hit that page as many times as you want but it's not going to get any different. So let's say that I'm convinced that This is the router cache problem. It's not the route cache. It's not the fact that it's static. The problem is that it is cached in the memory of the browser.

    So the way to bust the cache in the browser is to use the router hook, get the router, and then ask it to refresh. So let's try that. In order to do that, I'm gonna need to use a hook, which means I need to be a client component. Means I need to bring in useRouter and then run useRouter. And then I gotta put that on a button or something.

    So on my button, I'm going to make a button called bust that cache, and it's going to call router refresh. So let's give that a try. We'll again, build and start. Now we're stuck at 20. Let's go to our subroute.

    I'll try and bust the cache and go home. And again, I'm stuck at 20 because it's not an issue with the router cache. I just see so many folks think that this is the router cache when they should really be looking at that build to go and see that the route that they're looking at, in this case, slash, is static and it's not going to ever rebuild, not unless you revalidate it. So let's do that. So the real fix here is to create a server action.

    So I'll create a new server action. We'll call it Revalidate Home. I'll bring in Revalidate Path from Next Cache and then just Revalidate Slash. Now let's try that out. So I'll go to my sub-route.

    I'll bring in that RevalidateHome. This time I will go and await revalidate home. Of course I need to be an async function, make ourselves an async function, and I will build and run again. Now we've got 56, subroute and bust the cache, then go back to home, and now, we've actually incrementally rebuilt home. So we are going to get a different number each time I bust that cache.

    So there's 18, I can go backwards and forwards, hit 18, bust the cache, go home, and now it's 23. Cool, so that's the actual way to fix that problem. It's never been an issue with the router cache, it's only an issue with the router cache. Let's say I want to bust it and go home. So how would I do that?

    Well, that's actually pretty easy. We still have router around, so let's just do router.push slash. And now we can call this go home, because that's what it's going to do. It's going to revalidate slash and then go home. Get rid of the link, hit save.

    Again, we'll go and build and start. Now I've got 22. I go into my sub route, I go home and I've got 26. There we go. So I get new values each time.

    Awesome. So we're doing that revalidation, and we are also doing the navigation. Now the reason that revalidate path works, because when a server action is executed, what happens is the client posts back to the server with a next action ID. The server then looks at the incoming post request, sees that it's got a next action in the header, and then runs the appropriate next action, in this case revalidate home. Now if that server action does any kind of revalidation, revalidate tags or revalidate path, the response that goes back to the client has in the response header a revalidation section.

    And that is picked up then by the Next.js runtime on the client. And that's when the router cache gets invalidated on the client. But what if I don't run a server action? What if instead I have an API route that also does this revalidate home. Let's try that out.

    So I create a new API, revalidate home, route.ts, because it's going to have to be a route handler because it's an API. I basically handle it pretty much exactly the same way. It's a POST request, so I need to fetch to post that and then I revalidate path in there. I give back some random JSON response and that should be the same thing right? Well if I go back over here and instead of revalidate home I instead fetch revalidate home and await that fetch.

    I don't really need to look at the response or anything like that. Let's see what happens then. All right, we built and run. Let's try it out. Let's go to our sub-route.

    Now, we've got our go home. We started with 06 and we come back to 06. That's because when the API route returns, we're not actually adding that header in the response and the code isn't looking for that header in the response to revalidate the router cache. So this is a place where you would need to do a router refresh in here. So after we navigate to home, we want to do a router refresh.

    Now we'll build and start again. We'll start at our home, it's on five. We'll go to our sub-route and come back. And now we're getting the updated time. Now another reuse for that router refresh is actually on demand, going to just update the page.

    So let's go and try that out. Let's make that homepage dynamic now. Now to make it dynamic we could hit up headers or cookies or the search params or we could just export a constant called dynamic and just force this route to be dynamic. Do that, we say force dynamic. Now, when I build and start, you can see that the homepage is now dynamic as symbolized by that little f.

    Our API for Revalidate Home is also dynamic because it's a post route. Now let's go back to our arc and we can just hit refresh and we'll get the new time. Now we want to do that just periodically. We could create a new client component called timer and that timer wouldn't have any UI on it but it would run a use effect and that use effect would set an interval that where every second it would call that router refresh. All right let's give that a try.

    We'll add that to our page. Now we'll build and start again. Now because of that timer, we do a router refresh every second, we request new content from the server, and then we merge that into the existing DOM. So this is a nice flicker-free update. If I'm being honest, the React Router, which is the client-side cache of the AppRouter, really isn't something that you need to worry about in most circumstances it just kind of works.

    Most of the time you're just going to be doing a router.push to force someone to navigate if that's required or a router.refresh if you want to refresh the current route.