This blog covers how I fixed a caching bug in a Next.js App Router project. I ran into it while working on my personal blog, and the fix applies to Next.js 13 and up using the App Router.
Bug Description
Next.js has a default behavior where it caches the server-rendered result of a route on the server (Full Route Cache), and this cache can persist across sessions. That means a browser refresh on the client side doesn’t clear it.
Plus, navigating through the Next.js Link component performs a soft navigation, meaning Next.js intentionally tries to render cached content without fetching a new React Server Component payload. This can potentially lead to unexpected rendering behavior.
In my case, when the application is on /blog?tag=canvas and a browser refresh is performed, later navigations to /blog result in Next.js serving cached content from /blog?tag=canvas, which leads to incorrect page rendering, as shown in the following GIF.
Fix with Client Side Router Refresh
The first approach for a fix is to leverage the client-side router.refresh() API. We invoke router.refresh() when the current page mounts, which clears the Client Side Router Cache for that route and makes a new request to the server for an updated React Server Component payload. This subsequently revalidates the page content.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export function ClientComponent() {
const router = useRouter();
useEffect(() => {
// revalidates current path
router.refresh();
}, [])
return (<></>)
}
This approach fixes the issue, as shown in the above GIF. However, it's pretty easy to notice that it impacts rendering performance as we're programmatically triggering a refresh, it causes an extra render cycle. This leads to excessive rendering, as demonstrated in the following GIF using React Scan.
Fix with Server Side Revalidation
A more server-oriented and performant fix is to use the revalidatePath API on the server side, which revalidates the content of a specified path on demand.
"use server";
import { revalidatePath } from "next/cache";
// server action to revalidate '/blog' path
export async function revalidateBlog() {
revalidatePath("/blog");
}
To make this work, we need to create a server action that's triggered on navigation through links. This also means moving the Link component to the client side so we can attach a click event listener.
'use client';
import { revalidateBlog } from "app/blog/actions/actions";
import Link from "next/link";
export function ClientLinkComponent() {
const handleRevalidateBlogRoute = () => {
revalidateBlog()
};
return (
<Link
onClick={handleRevalidateBlogRoute}
href={'/blog'}
>
All
</Link>
);
}
We can now see from the React Scan results that rendering performance has improved.
And here’s a clearer demonstration of the fix result without using React Scan.