In a previous chapter we looked at .
// app\lib\getTodo.ts
type TodoT = {
id: number;
title: string;
completed: boolean;
};
export async function getTodo(id: number) {
'use cache';
console.log('Running getTodo with id: ', id);
const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const post: TodoT = await data.json();
return post;
}
// components\todo\Todo.tsx
type Props = {
id: number;
};
export async function Todo({ id }: Props) {
const todo = await getTodo(id);
return (
<div className='flex gap-2'>
<div className='font-bold'>{todo.id}.</div>
<div className='italic'>{todo.title}</div>
</div>
);
}
And the route (/chapter-13/todo):
// app\chapter-13\todo\page.tsx
export default async function TodoPage() {
return (
<>
<h1>Todo</h1>
<Todo id={1} />
<Todo id={1} />
<Todo id={2} />
</>
);
}
When we run next build, this is our terminal log:
├ ○ /chapter-13/todo 15m 1y
○ (Static) prerendered as static content
Two important things to note:
- This route is static, it is prerendered at build time.
<Todo />is not rendered at runtime, it is not partially prerendered. Everything is added to the static shell. - This route does not contain a
Suspenseboundary and the build does not error.
The reason behind this initially confused me. We will demystify it in this chapter.
Dynamic rendering inside PPR
Components become dynamic when they contain dynamic elements:
- Runtime APIs:
headers,cookies,params,searchParams
- Non-deterministic operations that require
connection. draftmode- Uncached data fetches.
Component that do not contain dynamic elements are rendered statically. Static rendering means that components and routes are prerendered at build time. Dynamic components are rendered server-side at request time.
Runtime APIs are driven by HTTP requests made by the user to your app's server. During static rendering at build time, no user request exists: there are no headers, cookies, or URL paths containing params or searchParams. Therefore, runtime APIs require dynamic rendering because they depend on an active request to yield data.
Another dynamic element is "uncached data fetching." But why does this require dynamic rendering? The term "uncached data" can be obscure; a clearer description is fresh data:
- Uncached data = fresh data.
- Cached data is not fresh.
How do you get fresh (uncached) data? By fetching the data when the user requests it. Server-side rendering at request time. So, fresh data requires dynamic rendering. Only at request time (now), can we ensure fresh data.
Partial prerendering
Next.js continuously optimizes page delivery. Instead of rendering an entire route at request time, it only runs the dynamic components at request time. Static components are prerended at build time into the static shell.
When a route only contains static components it is fully prerended at build time. When a route contains a mix of static and dynamic components, Next prerenders as much static content as possible into the static HTML shell. This shell will be served to the user at request. In the background, Next will render the dynamic content and stream it to the user where is will be merged into the DOM.
Suspense revisited
Creating a route with both static and dynamic components requires Suspense. Dynamic components are wrapped inside Suspense to mark the boundary: dynamic rendering starts/ends here.
At build time, Next evaluates each route. When dynamic elements are encountered, Next looks for the nearest Suspense boundary. Nothing inside this boundary will be statically prerendered; instead, Next places the boundary's fallback UI into the static shell.
In other words, everything inside the Suspense boundary is skipped. But, be careful! Suspense does NOT create dynamic content. Wrapping a component inside a Suspense boundary does not make the component render dynamically. The only things that makes components dynamic is the use of (all together now): runtime APIs (headers, cookies, searchParams and params), connection (for non-deterministic operations), draftMode and uncached data fetching.
Static Suspense example
Let's walk through a brief example using three static components:
// app/components/global/Header.tsx
export default function Header() {
return <header>**** Header ****</header>;
}
// app/components/global/Footer.tsx
export default function Footer() {
return <footer>**** Footer ****</footer>;
}
// app/components/global/Main.tsx
export default function Main() {
return <main>**** Main ****</main>;
}
And we use them in this route:
// app\chapter-14\static-with-suspense\page.tsx
export default function Page() {
return (
<>
<Header />
<Suspense fallback='Fallback for Main'>
<Main />
</Suspense>
<Footer />
</>
);
}
What happens when we run build? (Note, we is out. This includes the release of Instant Navigations.
It allows 'blocking routes' where you have dynamic components (uncached pending promise) without Suspense fallback UI.
// in page.tsx or layout.jsx
export const instant = false;
A use case for this would be e.g. a blogpost that doesn't need a loading skeleton. You prefer the browser to wait (blocking) for a server response.
Conclusion
This chapter explored the role of Suspense in Partial Prerendering and the Cache Components Model. At build time, Next evaluates each route. When it encounters an uncached pending promise (dynamic element) it looks for the nearest Suspense boundary. None of the components inside this boundary will be statically prerendered. Rendering is deferred and will happen dynamically, server-side at request time.
The Suspense boundary allows partial prerendering. Partial prerendering allows dynamic rendering. Dynamic rendering is needed for
- Runtime APIs like
headers,cookies,paramsandsearchParamsthat require a user request. - Uncached data fetches: fresh data.
In the next chapter, we take a look at how data cache functions within in the Cache Components model.
If you want to support my writing, you can donate with paypal.
SOCIAL SHARE CARD GENERATOR