Here's a quick recap:
Client components are components that:
- Use interactivity or event listeners.
- Use certain hooks:
- Lifecycle or state hooks.
- Custom hooks that use lifecycle of state hooks.
-
useSearchParams
hook. - Other hooks like
useRouter
orusePathname
.
- Use browser only API's.
- Are Class components.
A client component does not equal a client-side rendered component. All other components are by default server components. These are new and never render client-side.
Dynamic rendering means that routes are prerendered on the server at request time, not at build time. Next
dynamically renders routes when it encounters a dynamic fetch or dynamic functions:
-
headers()
orcookies()
-
useSearchParams
hook: (not always) -
searchParams
page prop
All other routes are statically rendered. That means that they are prerendered on the server at build time (next build
).
Before we combine these concepts we need one more tool, namely how do we test if a component is a server or a client component?
Testing server and client components
We actually already know how to test this because we already did it in the previous chapter: using logs. I wrote this chapter before I wrote the previous chapter, that is how I knew how to do it. I'm going to describe the process in more detail here because there are some restrictions to this method.
Some parts of the Next
docs confused me. They explicitly mention static rendering works differently for client and server components. Both are prerendered and cached.
Server and Client Components are rendered differently during Static Rendering:
Client Components have their HTML and JSON prerendered and cached on the server. The cached result is then sent to the client for hydration.
Server Components are rendered on the server by React, and their payload is used to generate HTML. The same rendered payload is also used to hydrate the components on the client, resulting in no JavaScript needed on the client.
Source:
Next
docs
This explanation did not work for me. It seems to be some technical detail so I kept looking. Eventually, I came across these lines:
- Components in the Server Component module graph are guaranteed to be only rendered on the server.
- Components in the Client Component module graph are primarily rendered on the client, but with Next.js, they can also be pre-rendered on the server and hydrated on the client.
So, server components are only rendered on the server, never on the client. Client components, as we also know from the previous chapter, run a bit on the server and a bit on the client. This realization led me to use logs to test.
But, I ran into some problems:
- The server components:
- Only log in the terminal the first time they are visited in development mode. The cached result is served on the second visit, hence no second log.
- Never log in a production build.
- The client components
- Always log in the console, both in development and in a production build.
- Only log in the terminal on a page refresh.
Conclusion: We can test if a component is client or server by adding a console log statement. But, we can only use this in development mode AND we need to refresh the page to get reliable results:
client component logs: terminal (server) + console (browser)
server component logs: terminal (server)
It's time to combine client and server components with static and dynamic rendering.
Static routes
A route is statically rendered when there are no dynamic functions or dynamic fetches in the route.
Let's make 2 examples. Note that we are using app router
now and also note the console.log statements added for testing.
Note: all tests are available on github.
// app/test2/static/server/page.js
export default function StaticServer() {
console.log('Rendering StaticServer');
return (
<div>
<h2>Server component in a static route</h2>
</div>
);
}
// app/test2/static/client/page.js
'use client';
export default function StaticClient() {
console.log('Rendering StaticClient');
return (
<div>
<h2>Client component in a static route</h2>
</div>
);
}
Running next build
yields the expected result. Both routes were statically rendered:
(Next cli)
Route (app)
├ ○ /test2/static/client
├ ○ /test2/static/server
The tests in development mode also return as expected. When visiting (and refreshing) route /test2/static/server
we get the 'Rendering StaticServer' in our terminal but not in the browser console. Route /test2/static/client
logs 'Rendering StaticClient' in our terminal and our console.
Dynamic rendering
A route is rendered dynamically when Next
detects dynamic fetches or dynamic function in the route. Dynamic functions are: headers
, cookies
, useSearchParams
(sometimes, see later) and the searchParams
page props.
There are some specifications.
- The
cookies
andheaders
functions are only callable in server components. If you call them in a client componentNext
throws an error. - The
useSearchParams
hook is only available in client components. Calling it in a server component will causeNext
to throw an error. - Finally, the
searchParams
page prop is available in both client and server components but is - obviously - only available for pages (page.js
in a route root).
Let's make a dynamic server component and run build:
// app/test2/dynamic/server/page.js
export default function DynamicServer({ searchParams }) {
console.log('Test 2: rendering DynamicServer');
return (
<div>
<h2>Server component in a dynamic route</h2>
</div>
);
}
Next cli
unexpectedly reports this route as static:
(Next cli)
○ /test2/dynamic/server
So, it seems that Next
is pretty smart about this. It recognizes that the searchParams
props wasn't used and hence this component will not trigger a dynamic render. Let's try this:
// app/test2/dynamic/server/page.js
export default function DynamicServer({ searchParams }) {
console.log('Test 2: rendering DynamicServer');
const params = searchParams;
return (
<div>
<h2>Server component in a dynamic route</h2>
</div>
);
}
(Next cli)
○ /test2/dynamic/server
Again, Next
was pretty smart. There is no need for this component to trigger a dynamic rendering and Next
recognized this. New update:
// app/test2/dynamic/server/page.js
export default function DynamicServer({ searchParams }) {
console.log('Test 2: rendering DynamicServer');
const foobar = searchParams?.foobar;
return (
<div>
<h2>Server component in a dynamic route</h2>
</div>
);
}
(Next cli)
Route (app)
...
λ /test2/dynamic/server
Legend
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
Success, our DynamicServer
component / page triggered a dynamic route rendering. I took some time to show you how I debugged this problem so we could learn that these dynamic function won't automatically trigger dynamic rendering and to demonstrate the usefulness of testing.
Lastly, let's create a DynamicClient
component and run build:
// app/test2/dynamic/client/page.js
'use client';
export default function DynamicClient({ searchParams }) {
console.log('Test 2: rendering DynamicClient');
const foobar = searchParams?.foobar;
return (
<div>
<h2>Client component in a dynamic route</h2>
</div>
);
}
Next cli
output:
(Next cli)
Route (app)
├ λ /test2/dynamic/client
├ λ /test2/dynamic/server
Confirming both dynamicServer
and dynamicClient
are in a dynamic render route. As an extra test, we look in the build folder: .next/server/app/test2/dynamic
and there are no server.html
or client.html
files in there. This is as expected.
Running next dev
to test client or server component also yields the expected results.
- Route
test2/dynamicServer
(+ page refresh) loggedTest 2: rendering DynamicServer
in the terminal but not in the client. - Route
test2/dynamicClient
(+ page refresh) loggedTest 2: rendering DynamicClient
in the terminal and in the browser console.
Conclusion
We spent some of our time in this chapter trying to figure out how to test if a component is a client or a server component. We found a flawed but usable method.
We then made 4 test: a statically rendered client and server component and a dynamically rendered client and server component.
Every chapter till now was pretty much a setup for the following chapters where we will look into more practical thing like nesting and passing props but also quirky details. I will walk you through all of these using examples.
Next chapter: Nesting client and server components in Next 13
Top comments (2)
Hey, thank you for sharing this valuable information. One request, if possible, could you also create a mini series on caching in next.js and how it interacts with static and dynamic rendering. I have read the documentation multiple times but still find things difficult to follow. Nonetheless, thank you once again for this mini series.
I don't have direct plans to write this but I would highly recommend to just test it out yourself. Write a little app and just test it out.