We talked about incremental static regeneration before, it's when a statically rendered route is updated without having to rebuild it. It updates, server-side at request time. But that is just updating existing routes. ISR has an extra trick. You can also add new routes to the full route cache.
An example
It's best explained using an example. Note: the examples are available on github.
Let's say you have a blog app with five blog posts. When deploying the app, it will first run a build (next build) and then start up the server (next start). The five posts were statically rendered and are now in the full route cache. Each request will be served from this cache.
But how do we add content? Let's say we wrote a sixth blogpost in the CMS and published it. What now? We want a statically generated post but how do we get it on the Next server? A redeploy with a new build?
Maybe the CMS calls a webhook on publish. We can catch that in a route handler and call ... what exactly? We can't use revalidatePath because there is no path (the post doesn't exist in the initial build). revalidateTag('post-6', "max") maybe? No, there is no cache tag for 'post-6' yet. So, that's our problem, we need to add prerendered routes.
We set up a blog example: a blog home page and then posts in a dynamic route segment ([slug]).
app
blog
page.tsx
[id]
page.tsx
This is our blog home page.
// app/blog/page.tsx
import Link from 'next/link';
export type PostT = {
id: number;
title: string;
body: string;
};
export async function getPosts() {
const data = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: PostT[] = await data.json(); // 100 posts
return posts.slice(0, 5); // return 5 posts
}
export default async function BlogHome() {
const posts = await getPosts(); // 5 posts
return (
<div>
<h1 className='text-xl font-bold mb-2'>Blog</h1>
<ol className='list-inside list-decimal'>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.id}`} className='underline text-blue-500'>
{post.title}
</Link>
</li>
))}
<li>
<Link href={`/blog/6`} className='underline text-blue-500'>
post 6
</Link>
</li>
</ol>
</div>
);
}
Quick overview: We're trying to simulate that we have a CMS that returns 5 posts. So, we take the 100 posts that JSONPlaceholder api returns and only use the first 5. We then render these 5 posts out as links to individual blog posts. Finally, I manually added a 6th link. We will need this in a bit to run some tests.
Here is our dynamic route segment (f.e. /blog/1):
// app/blog/[id]/page.tsx
import SinglePost from '@/components/blog/SinglePost';
import { getPosts } from '../page';
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id.toString() }));
}
export const dynamicParams = false;
export default async function Page({ params }: PageProps<'/blog/[id]'>) {
const { id } = await params;
return <SinglePost id={id} />;
}
You probably are likely familiar with dynamic route segments but here's a quick recap. We want to statically render this route (/blog/[id]/page.tsx). To let Next know what the ID is, we use generateStaticParams. This returns an array of the IDs we have for our five posts:
[
{ id: 1 },
{ id: 2 },
//...
];
This enables Next to statically render these five posts at build time. Note the export const dynamicParams = false;. This actually disables the addition of new routes, more on this later. Finally, we render the post content in a <SinglePost /> component. It makes a fetch for a single post with given id and prints out title and content.
// components/blog/Post.tsx
import { PostT } from '@/app/blog/page';
import Link from 'next/link';
type Props = {
id: string;
};
export async function getPost(id: string) {
const data = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post: PostT = await data.json();
return post;
}
export default async function SinglePost({ id }: Props) {
const post = await getPost(id);
return (
<div>
<h1 className='text-xl font-bold mb-2'>{post.title}</h1>
<p>{post.body}</p>
<Link href='/blog' className='text-blue-400 underline block mt-4'>
back
</Link>
</div>
);
}
Run build
So, we have our setup. Let's now see what happens when we run next build:
├ ○ /blog
├ ● /blog/[id]
│ ├ /blog/1
│ ├ /blog/2
│ ├ /blog/3
│ └ [+2 more paths]
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand
The build log confirms what we expected, the blog home and all individual posts were rendered statically. Note that Next distinguishes between Static and SSG.
As we did earlier, let's also take a look at the .next build folder. We find the following files:
.next
server
app
blog.html
rsc.html
blog
1.html
1.rsc
...
5.html
5.rsc
So, for each of our 6 pages, an HTML and .src file were created. This is what we expect from static rendering.
next start
Let's now run next start and run locally in production mode. The page http://localhost:3000/blog appears as expected. Five generated links and one hard coded link. The five generated links work properly and lead to the relevant posts. Clicking the hardcoded link to /blog/6 (that doesn't exist in our prerendered build) leads us to a 404 not found page. This is as expected.
Now, we go back to the dynamic route segment (/blog/[id]/page.tsx) and change the route segment option dynamicParams to true. Note that true is the default value, so we could omit it:
export const dynamicParams = true; // true | false
// same as
// export const dynamicParams = true; // true | false
After rebuilding and restarting, the build log is the same and everything works as before except the blog/6 link. That no longer leads to a 404 but to an actual blog post:
<!-- http://localhost:3000/blog/6 -->
<div>
<h1 class="text-xl font-bold mb-2">dolorem eum magni eos aperiam quia</h1>
<p>
ut aspernatur corporis harum nihil quis provident sequi mollitia nobis
aliquid molestiae perspiciatis et ea nemo ab reprehenderit accusantium quas
voluptate dolores velit et doloremque molestiae
</p>
<a class="text-blue-400 underline block mt-4" href="/blog">back</a>
</div>
This is great, we were able to create a new page. But there is something else to see here. Our .next build folder now contains 6.html and 6.rsc.
.next\server\app\blog\6.html
.next\server\app\blog\6.rsc
This is an extra function of incremental static regeneration. We added a route to the full route cache, not at build time but at request time. This prerendered route will now be served to all users. Once again, a user made a request to a route that was not prerendered. Next then freshly rendered the route - server-side at request time and added it to our full route cache. So, ISR allows us to add content to the full route cache at runtime. And that is awesome!
Some improvements
I want to take the rest of the chapter as an opportunity to apply some of the things we learned about caching thus far.
Blog home
On our blog home page, we made a fetch with no caching option - the auto option. That means no data cache was created. Nextmade a fetch, used it to generate HTML and rsc but did not cache the fetch result. In a real project, that could be a problem: when we publish a new blogpost, we would like it to appear on our home page.How would we solve this?
- We could make the blog homepage dynamic and set no-store to the fetch. Every request would generate a fresh result. It would work.
-
revalidatePathorrevalidateTagcould work. CMS (publish post) -> webhook -> route handler ->revalidatePath('/app/blog')orrevalidateTag(posts). The cached route is marked as stale andNextregenerates the page. We now have a link to our new blog post. - Time based revalidation could also work, e.g.
fetch('https://jsonplaceholder.typicode.com/posts', { revalidate: 360 }). Every hour the data becomes stale and the next request will trigger a revalidation. Result: our new link appears.
Any form of caching on the posts fetch would have another benefit. We used getPosts inside our blog home page but also inside generateStaticParams in our dynamic route segment (/blog/[id]/page.tsx). This means that at build time, Next only fetched it once and used the data cache for the second one.
Blog post
So, ISR allows us to add content to our full route cache and to update our full route cache. Right now, we don't have that functionality in our post. If we wrote a typo for example, there is no way to fix it. We need revalidation.
How? Depends, you know the options: time based or on demand revalidation. Determine what you need and find a suitable solution.
ISR setup
Next allows ISR by default. This has some consequences. What if we visit blog/foobar? This should fail. Our fetch getPost wouldn't return a post but - depending on the CMS - something empty or an error. Our <SinglePost /> component would then crash trying to render this out as a post. To solve this, check the data and use notFound().
// has post?
if (!post) {
notFound();
}
Also note that if you do not want ISR, you must turn it off: export const dynamicParams = false. This is important because crawlers or malicious users will otherwise make weird requests that force your server to try and render non-existent pages.
Onwards
So, we learned how to add statically rendered routes to full route cache, not at build time but at request time. This is a very powerful tool especially when combined with cache invalidation.
In the next chapters we learn about other cache mechanisms Next provides.
If you want to support my writing, you can donate with paypal.
Top comments (0)