Fun With Next.js 13 Server Components
Next.js 13 has landed in a somewhat confusing way. Many remarkable things have been added; however, a good part is still Beta. Nevertheless, the Beta features give us important signals on how the future of Next.js will be shaped, so there are good reasons to keep a close eye on them, even if you’re going to wait to adopt them.
This article is part of a series of experiences about the Beta features. Let’s play with Server Components today.
Making server components the default option is arguably the boldest change made in Next.js 13. The goal of server component is to reduce the size of JS shipped to the client by keeping component code only on the server side. I.e., the rendering happens and only happens on the server side, even if the loading of the component is triggered on the client side (via client-side routing). It’s quite a big paradigm shift.
I first got to know React Server Components over a year ago from this video (watch later, it’s pretty long 😄):
It feels quite “reasearch-y” by then, so I was shocked when seeing that Next.js is already betting its future on it now. Time flies and the fantastic engineers from React must have done some really great work, so I created a shiny new Next.js 13 project to play with it.
1 2 |
npx create-next-app@latest <span class="nt">--experimental-app</span> <span class="nt">--ts</span> <span class="nt">--eslint</span> next13-server-components |
Let’s have some fun playing with the project. You can find the full project code here.
Server Component
The first difference noticed is that a new app
folder now sits along with our old friend page
. I’ll save the routing changes to another article, but what’s worth mentioning for now is that every component under the app
folder is, by default, a server component, meaning that it’s rendered on the server side, and its code stays on the server side.
Let’s create our very first server component now:
1 2 |
<span class="c1">// app/server/page.tsx</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Server</span><span class="p">()</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Server page rendering: this should only be printed on the server</span><span class="dl">'</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Server Page<span class="p"></</span><span class="nt">h1</span><span class="p">></span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>My secret key: <span class="si">{</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">MY_SECRET_ENV</span><span class="si">}</span><span class="p"></</span><span class="nt">p</span><span class="p">></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p">);</span> <span class="p">}</span> |
If you access the /server
route, whether by a fresh browser load or client-side routing, you’ll only see the line of log printed in your server console but never in the browser console. The environment variable value is fetched from the server side as well.
Looking at network traffic in the browser, you’ll see the content of the Server component is loaded via a remote call which returns an octet stream of JSON data of the render result:
1 2 3 |
<span class="p">{</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="nl">"childProp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"current"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"$"</span><span class="p">,</span><span class="w"> </span><span class="s2">"div"</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">[</span><span class="s2">"$"</span><span class="p">,</span><span class="w"> </span><span class="s2">"h1"</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Server Page"</span><span class="w"> </span><span class="p">}],</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"$"</span><span class="p">,</span><span class="w"> </span><span class="s2">"p"</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"My secret key: "</span><span class="p">,</span><span class="w"> </span><span class="s2">"abc123"</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span> |
Rendering a server component is literally an API call to get serialized virtual DOM and then materialize it in the browser.
The most important thing to remember is that server components are for rendering non-interactive content, so there are no event handlers, no React hooks, and no browser-only APIs.
The most significant benefit is you can freely access any backend resource and secrets in server components. It’s safer (data don’t leak) and faster (code doesn’t leak).
Client Component
To make a client component, you’ll need to mark it so explicitly with use client
:
1 2 3 |
<span class="c1">// app/client/page.tsx</span> <span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Client</span><span class="p">()</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span> <span class="dl">'</span><span class="s1">Client page rendering: this should only be printed on the server during ssr, and client when routing</span><span class="dl">'</span> <span class="p">);</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Client component rendered</span><span class="dl">'</span><span class="p">);</span> <span class="p">});</span> <span class="k">return</span> <span class="p">(</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Client Page<span class="p"></</span><span class="nt">h1</span><span class="p">></span> <span class="si">{</span><span class="cm">/* Uncommenting this will result in an error complaining about inconsistent rendering between client and server, which is very true */</span><span class="si">}</span> <span class="si">{</span><span class="cm">/* <p>My secret env: {process.env.MY_SECRET_ENV}</p> */</span><span class="si">}</span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p">);</span> <span class="p">}</span> |
As you may already anticipate, this gives you a similar behavior to the previous Next.js versions. When the page is first loaded, it’s rendered by SSR, so you should see the first log in the server console; during client-side routing, both log messages will appear in the browser console.
Mix and Match
One of the biggest differences between server component and SSR is that SSR is at page level, while Server Component, as its name says, is at component level. This means you can mix and match server and client components in a render tree as you wish.
1 2 3 4 5 |
<span class="c1">// A server page containing client component and nested server component</span> <span class="c1">// app/mixmatch/page.tsx</span> <span class="k">import</span> <span class="nx">Client</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">NestedServer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./nested-server</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">MixMatchPage</span><span class="p">()</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">MixMatchPage rendering</span><span class="dl">'</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Server Page<span class="p"></</span><span class="nt">h1</span><span class="p">></span> <span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"box"</span><span class="p">></span> <span class="p"><</span><span class="nc">Client</span> <span class="na">message</span><span class="p">=</span><span class="s">"A message from server"</span><span class="p">></span> <span class="p"><</span><span class="nc">NestedServer</span> <span class="p">/></span> <span class="p"></</span><span class="nc">Client</span><span class="p">></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p">);</span> <span class="p">}</span> |
In a mixed scenario like this, server and client components get rendered independently, and the results are assembled by React runtime. Props passed from server components to client ones are serialized across the network (and need to be serializable).
Server Components Can Degenerate
One caution you need to take is that if a server component is directly imported into a client one, it silently degenerates into a client component.
Let’s revise the previous example slightly to observe it:
1 2 3 |
<span class="c1">// app/degenerate/page.tsx</span> <span class="k">import</span> <span class="nx">Client</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./client</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">MixMatchPage</span><span class="p">()</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">MixMatchPage rendering</span><span class="dl">'</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>MixMatch Server Page<span class="p"></</span><span class="nt">h1</span><span class="p">></span> <span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"box-blue"</span><span class="p">></span> <span class="p"><</span><span class="nc">Client</span> <span class="na">message</span><span class="p">=</span><span class="s">"A message from server"</span> <span class="p">/></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p">);</span> <span class="p">}</span> |
1 2 3 |
<span class="c1">// app/degenerate/client.tsx</span> <span class="dl">'</span><span class="s1">use client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="nx">NestedServer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./nested-server</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Client</span><span class="p">({</span> <span class="nx">message</span> <span class="p">}:</span> <span class="p">{</span> <span class="nl">message</span><span class="p">:</span> <span class="kr">string</span> <span class="p">})</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Client component rendering</span><span class="dl">'</span><span class="p">);</span> <span class="k">return</span> <span class="p">(</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span> <span class="p"><</span><span class="nt">h2</span><span class="p">></span>Client Child<span class="p"></</span><span class="nt">h2</span><span class="p">></span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Message from parent: <span class="si">{</span><span class="nx">message</span><span class="si">}</span><span class="p"></</span><span class="nt">p</span><span class="p">></span> <span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"box-blue"</span><span class="p">></span> <span class="p"><</span><span class="nc">NestedServer</span> <span class="p">/></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="p">);</span> <span class="p">}</span> |
If you check out the log, you’ll see NestedServer
has “degenerated” and is now rendered by the browser.
Is It a Better Future?
Next.js is trying everything it can to move things to the server side, exactly how people did web development two decades ago 😄. So now we’re completing a full circle, but with greatly improved development experiences and end-user experiences.
For the end users, it’s a clear win since computing on the server side is faster and more reliable. The result will be a much more rapid first content paint.
For developers, the paradigm shift will be mentally challenging, mixed with confusion, bugs, and anti-patterns. It will be a hell of a journey.
In the End
Thank you for your time reading to the end ❤️.
This is the first article of a series about the Beta features of Next.js 13. If you like our posts, remember to follow us for updates at the first opportunity.
ZenStack is a schema-first toolkit for building CRUD services in Next.js projects. Our goal is to let you save time writing boilerplate code and focus on building what matters – the user experience.
Find us on GitHub:
A schema-first toolkit for building CRUD services in Next.js projects
📣 Our Discord Server is Live
JOIN US to chat about questions, bugs, plans, or anything off the top of your head.
What is ZenStack?
ZenStack is a toolkit for modeling data and access policies in full-stack development with Next.js and Typescript. It takes a schema-first approach to simplify the construction of CRUD services.
Next.js is an excellent full-stack framework. However, building the backend part of a web app is still quite challenging. For example, implementing CRUD services efficiently and securely is tricky and not fun.
ZenStack simplifies it by providing:
- An intuitive data modeling language for defining data types, relations, and access policies
1 2 3 4 5 6 7 8 9 10 |
<span class="pl-s1"><span class="pl-k">model</span> <span class="pl-en">User</span> {</span> <span class="pl-s1"> <span class="pl-smi">id</span> <span class="pl-c1">String</span> <span class="pl-s1"><span class="pl-en"><a class="mentioned-user" href="https://dev.to/id">@id</a></span></span> <span class="pl-s1"><span class="pl-en"><a class="mentioned-user" href="https://dev.to/default">@default</a></span>(<span class="pl-s1"><span class="pl-c1">cuid</span>()</span>)</span></span> <span class="pl-s1"> <span class="pl-smi">email</span> <span class="pl-c1">String</span> <span class="pl-s1"><span class="pl-en">@unique</span></span></span> <span class="pl-s1"> <span class="pl-c">// one-to-many relation to Post</span></span> <span class="pl-s1"> <span class="pl-smi">posts</span> <span class="pl-c1">Post</span><span class="pl-k">[]</span></span> <span class="pl-s1">}</span> <span class="pl-s1"><span class="pl-k">model</span> <span class="pl-en">Post</span> {</span> <span class="pl-s1"> <span class="pl-smi">id</span> <span class="pl-c1">String</span> <span class="pl-s1"><span class="pl-en"><a class="mentioned-user" href="https://dev.to/id">@id</a></span></span> <span class="pl-s1"><span class="pl-en"><a class="mentioned-user" href="https://dev.to/default">@default</a></span>(<span class="pl-s1"><span class="pl-c1">cuid</span>()</span>)</span></span> <span class="pl-s1"> <span class="pl-smi">title</span> <span class="pl-c1">String</span></span> <span class="pl-s1"> <span class="pl-smi">content</span> <span class="pl-c1">String</span></span> <span class="pl-s1"> <span class="pl-smi">published</span> <span class="pl-c1">Boolean</span> <span class="pl-s1"><span class="pl-en"><a class="mentioned-user" href="https://dev.to/default">@default</a></span>(<span class="pl-c1">false</span>)</span></span> <span class="pl-s1"> <span class="pl-c">// one-to-many</span></span> |
Source: https://dev.to/zenstack/fun-with-nextjs-13-server-components-o37
