Better Programming

Advice for programmers.

Follow publication

Grok React Server Component by Quizzes

Boost performance and reduce your client-side code size

Herrington Darkholme
Better Programming
Published in
13 min readMay 8, 2023

--

Photo by JESHOOTS.COM on Unsplash

React Server Component is a new React architecture introduced by the React team at the end of 2020. It enables developers to render components on the server side, thereby boosting performance and reducing client-side code size.

Though already over two years old, this innovation still poses some novel challenges and issues. RSC is arguably the hottest topic in the React community, but many React users are still baffled or perplexed by it.

Dan Abramov presented three quizzes about React Server Component to help the audience to understand the new technology better. Unfortunately, about 45% failed to answer these quizzes correctly. RSC is hard, and it’s not only you find it hard.

I will explain how React Server Component works in this article from multiple perspectives. We will first recap the Server Component’s RFC, then show the barebone of the wiring protocol, and finally explain the quizzes. I hope this can help you understand the mechanism of RSC.

TLDR: React will render Server Components as JSON-like UIs and stream them to browsers. Client Components will be rendered as a placeholder referencing the actual component code.

Both JSONs and references will be streamed to the browser for reconciliation and DOM updates.

The best way to understand tech is to use it… even in a quiz

The Three RSC Quizzes

We will first go over Dan’s quizzes.

First Quiz

Here’s the quiz from the tweet https://twitter.com/dan_abramov/status/1648923232937058304

function Note({ note }) {
return (
<Toggle>
<Details note={note} />
</Toggle>
}

The only Client component out of these is Toogle. It has state (isOn, initially false). It returns <>{isOn ? children : null}</>. What happens when you setIsOn(true)?

  • Details gets fetched.
  • Details appears instantly.

Second Quiz

https://twitter.com/dan_abramov/status/1648926252328263686

Now, say isOn is true. You’ve edited the note and told the router to “refresh” the route. This refetches the RSC tree for this route, and your Note server component receives a note prop with the latest DB content.

(1) Does Toggle state get reset?
(2) Does Details show fresh content?

  • (1) yes and (2) yes
  • (1) yes and (2) no
  • (1) no and (2) yes
  • (1) no and (2) no

Third Quiz

https://twitter.com/dan_abramov/status/1648928302352982016

Here’s a little twist:

<Layout
left={<Sidebar />}
right={<Content />}
/>

All are Server components, but now your want to add a bit of state to Layout, like column width, which changes on mouse drag.

Can you make Layout a Client component? If yes, what happens on a drag?

  • No, this isn’t allowed.
  • They all refetch on a drag
  • No refetches on a drag

Readers who are not familiar with RSC may be completely confused after reading these three questions. Don’t worry. We will first briefly introduce what RSC is. You can skip the section safely if you already know RSC.

What is React Server Component?

React Server Component is a special React component that does not run on the browser side but instead on the server side to directly access the server’s data and resources without accessing APIs like REST or GraphQL.

React Server Component is a pattern that can help us reduce the number of network requests and the data size, thereby improving the page loading speed and user experience. React Server Component can also serve dynamic content to users according to different request parameters without recompiling or deploying.

React Server Component aims to let developers build applications that span the server and client, combining the rich interactivity of client-side applications and the optimized performance of traditional server rendering.

React Server Component can solve some problems that existing technologies cannot solve or cannot solve well, such as:

  • Zero package size: React Server Component’s code only runs on the server side and will never be downloaded on the client side, so it does not affect the client’s package size and startup time. The client only receives the rendered results of RSC.
  • Full access to backend: React Server Component can directly access backend data sources, such as databases, file systems, or microservices, without additional API endpoints.
  • Automatic code splitting: React Server Component can dynamically choose which client components to render so that the client only downloads the necessary code.
  • No client-server waterfall: React Server Component can load data on the server and pass it as props to client components, thus avoiding the client-server waterfall problem.
  • Avoid abstraction tax: React Server Component can use native JavaScript syntax and features, such as async and await, without using specific libraries or frameworks to implement data fetching or rendering logic.

Server Component and Client Component

To grasp how RSC works, we must first learn about two major concepts in RSC: server-side components (Server Component) and client-side components (Client Component).

Server Component

As the name suggests, server components run only once per request on the server, so they have no state and cannot use features that only exist on the client. More specifically:

  • ❌ You cannot use state and side effects. Server components (conceptually) run only once per request on the server. So useState, useReducer, useEffect and useLayoutEffect are not supported. You cannot use custom hooks that also depend on state or side effects.
  • ❌ You cannot use browser-specific APIs, such as DOM (unless you polyfill them on the server).
  • ✅ You can use async/await to access server data sources, such as databases, internal (micro) services, file systems, etc.
  • You can render other server components, native elements (div, span, etc.) or client components.

Developers can also create some custom hooks or libraries designed for the server. All rules for server components apply. For example, we can create a server hook to provide utility functions for accessing server databases.

Client Component

Client Component is a regular React component that follows all the previous React rules. The only new restriction is that they cannot import server components.

  • ❌ Client components cannot import server components. However, server components can pass another server component as child to a client component.
  • ❌ Client components cannot use server-only data sources.
  • ✅ You can use state and side effects, as well as custom React hooks.
  • ✅ You can use browser APIs.

Here we must highlight the nested structure of server and client components. Client components can access server components as children but not by importing them directly.

For example, you can write code like <ClientTabBar><ServerTabContent/></ClientTabBar>. From the perspective of the client component, its child component will be a rendered tree, such as the output of ServerTabContent.
This means server and client components can be nested and interleaved at any level. We will explain this design in later quizzes.

How RSC works?

Server components and client components are fairly easy features by themselves, but how do they work in tandem? This leads us to two additional concepts in RSC: phase and platform.

RSC rendering is divided into two major phases: initial loading and view updating.

There are also two platforms for RSC: server and browser.

Remember that although server components run only on the server, the browser still needs to recognize them for view creation or updating. This also applies to client components.

Initial Loading

On Server

  • [Framework] The framework’s routing matches the requested URL with a server component, passing route parameters as props to the component. Then it calls React to render the component and its props.
  • [React] React renders the root server component and recursively renders any child components that are also server components.
  • [React] Rendering stops at native and client components (e.g., div or span). Native components are transformed into a JSON description of the UI. Client components are streamed in serialized props, plus a reference to the component code.
  • [Framework] The framework is responsible for streaming the rendered output to the client as React renders each UI unit.

React uses a JSON-like data structure to describe the rendered UI by default instead of HTML. This makes it easier to reconcile new data with existing client components. However, frameworks can also use “server-side rendering” (SSR) to stream the initial render as HTML and speed up the initial non-interactive page display.

When a server component suspends on the server, React will stop rendering that subtree and send a placeholder value to the browser. React will resume rendering the component and stream the actual component result to the browser when the component is ready (unsuspend). You can imagine the data streamed to the browser as JSON but with slots for suspended components. The values for those slots are sent as extra items in the response stream later.

To understand this paragraph better, let’s examine an example and learn more about the RSC protocol. Remember that this paragraph might not be exact, and RSC implementation might vary.

Suppose we want to render a div with a text and a client component as children.

<div>
Hello World
<ClientComponent/>
</div>

After calling React.createElement, a data structure similar to the following is generated:

{
$$typeof: Symbol(react.element),
// element tag
type: "div",
props: {
children: [
// static text
"Hello World",
// client compoennt
{
$$typeof: Symbol(react.module.reference),
type: {
name: "ClientComponent",
filename: "./src/ClientComponent.js"
},
},
]
},
}

This data structure will be streamed to the browser in a format like this:

// Sever Component Ouptut
["$","div",null,{"children":["Hello World", ["$","$L1",null,{}]]}]
// Client Component Reference
1:I{"id":"./src/ClientComponent.js","chunks":["client1"],"name":"ClientComponent","async":false}

The first array shows the output of a Server Component, which resembles React’s internal data structure, except that it is an array instead of an object.

The array element $ represents createElement, and the following element {children: xxx} represents props. The first child "Hello World" is directly sent to the browser as a string since it is a text node. L1 is a placeholder that corresponds to the 1 in 1:I below. The data of 1:I will be filled into L1‘s slot later.

You might be curious about what the I means. In React Server Component’s protocol, I represents ClientReferenceMetadata, a data structure helping the browser to find the correct script entry to the client component.
1:I’s output is a reference to the client component, which contains the script name, chunk name, and export name, for the browser runtime (such as webpack) to import the client component code dynamically.

In summary, a Server Component will be rendered into JSON-like data representing UI, while client components will be converted into JSON data expressing script references.

On Browser

  • [Framework] On the client side, the framework receives the streaming UI data and uses React to render it on the page
  • [React] React deserializes the response and renders native elements and client components.
  • [React] Once all client components and all server component outputs have been loaded, the final UI state will be displayed to the user. All Suspense boundaries have been revealed by then.

Note that browser rendering is progressive. React does not need to wait for the entire stream to complete before displaying some content. Suspense allows developers to display meaningful loading states while client component code and server components fetch data.

View Updating

Server components can also update the view to reflect the latest data change. Note that developers do not request server components one by one: one component per request. Instead, the entire subtree will be refetched at once, given some initial server components and props. This usually involves integration with routing and script bundling, as with initial loading.

On Browser

  • [App] When the application changes state or changes routes, it requests the server to refetch the new UI for the changed Server Component.
  • [Framework] The framework coordinates sending the new route and props to the appropriate API endpoint, requesting the rendering result.

On Server

  • [Framework] The endpoint receives the request and matches it with the requested server component. Then it calls React to render the component and props.
  • [React] React renders the component to the destination, with different rendering strategies for components and initial loading.
  • [Framework] The framework is responsible for progressively streaming the response data to the browser.

On Browser

  • [Framework] The framework receives the streaming response and triggers a rerender of the route with the new rendering output.
  • [React] React reconciles the new rendering output with the existing components on the screen. Because the description of UI is data, React can merge new props into existing components, preserving important UI states such as focus or input or triggering CSS transitions on top of existing content. This is a key reason server components return UI output as data (“virtual DOM”) rather than HTML.

Summary So Far

This section is very long, but we can summarize the working principle of RSC in one sentence.

The client Component will be rendered into a script reference, Server Component will be streamed into a JSON-like UI, Server Component with async/await will be replaced by a placeholder first and then streamed to the browser after being resolved.

The table below summarizes the essence of React Server Component rendering:

Three Quizzes, Three Features

Now let’s see how the above rendering process is applied in the RSC quizzes!

This article will combine these three questions to explain the three major features of RSC: rendering completeness, state consistency, and commutative client/server component.

Rendering Completeness

The first quiz:

function Note({ note }) {
return (
<Toggle>
<Details note={note} />
</Toggle>
}

Let’s write some more components to provide context for this question. Our Toggle component looks like this:

"use client";

import { useState } from "react"

export function Toggle(props) {
const [isOn, setIsOn] = useState(false)
return (
<div>
<button onClick={() => setIsOn(on => !on)}>Toggle</button>
<div>{isOn ? "on" : "off"}</div>
<div>{isOn ? props.children : <p>not showing children</p>}</div>
</div>
)
}

And out Details component looks like this:

export async function Details(props) {
const details = await getDetails(props.note);
return <div>{details}</div>;
}

async function getDetails(note: string) {
await new Promise((resolve) => setTimeout(resolve, 2000));
return `Details for ${note}`;
}

In this example, Note and Details are server components. Toggle is a client component, but its children Details appears directly under the server component Note. So the component tree will be rendered into a data structure like this:

{
$$typeof: Symbol(react.element),
// note the type
type: {
$$typeof: Symbol(react.module.reference),
name: "default",
filename: "./Toggle.js"
},
props: { children: [
// children, note the type
{
$$typeof: Symbol(react.element),
type: Details, // Details is rendered!
props: { note: note },
}
] },
}

Note that Details is always rendered on the server side and delivered to the browser.

Details is not used when Toggle is rendered on the client side, but its rendering result is still sent to the client side.

Even though Details is an asynchronous server component that uses async/await, it can still be sent to the browser after it finishes loading, thanks to the streaming feature of React Server Component.

When the user changes the state, the client can directly use pre-rendered results from the server because the props of Details are the same as those rendered by the server. Therefore, the answer to this question is that Details will appear immediately.

This question reveals the “completeness” of React Server Component: as long as the component appears under the render function of the server-side component, it will be rendered regardless of its usage on the client side.

State Consistency

Now assume that isOn is true. You edit the note and tell the router to “refresh” the route. This will re-fetch the RSC tree for this route, and your Note server component will receive a note attribute with the latest database content.

The second question reveals the consistency of RSC. When the Toggle component changes props on the client side. This change is synchronized between both the server component and the client component. When the note changes in the <Details note={note} />, React will detect the change and sends a request to the server for the new Details rendering data.

Meanwhile, the state of the client component Toggle is kept as is. So the state is not reset in the browser. The browser will insert the old Toggle instance into the slot in the new Details UI fetched from server.

Thus, the design of RSC ensures that the state of the application is consistent across both the server and browser.

3. Commutative Client/Server Component

For the third question, let’s also expand the question code for context:

function App() {
return (
<Layout
left={<Sidebar />}
right={<Content />}
/>
)
}

Layout component is a server component.

// Server Component
export function Layout(props: {
left: React.ReactNode
right: React.ReactNode
}) {
return (
<div>
<div>
<div style={{ width: `${width}px` }}>{props.left}</div>
<div style={{ width: `${500 - width}px` }}>{props.right}</div>
</div>
</div>
)
}

Let’s rewrite it to a client component that useState(no pun intended). In this example, the width is changed on the client side by sliding the range input. (The implementation detail is inconsequential here).

"use client"

import { useState } from "react"

export function Layout(props: {
left: React.ReactNode
right: React.ReactNode
}) {
const [width, setWidth] = useState(200)
return (
<div>
<input
type="range"
step={1}
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
/>
<div>
<div style={{ width: `${width}px` }}>{props.left}</div>
<div style={{ width: `${500 - width}px` }}>{props.right}</div>
</div>
</div>
)
}

We can change the Layout from the server component to the client component without touching the App component.

The only thing that changed is how the App is rendered on the server side. See the output data structure below:

 {
$$typeof: Symbol(react.element),
- type: Layout,
+ type: {
+ $$typeof: Symbol(react.module.reference),
+ name: "default",
+ filename: "./Layout.js"
+ },
props: {
left: {
$$typeof: Symbol(react.element),
type: Sidebar,
},
right: {
$$typeof: Symbol(react.element),
type: Content,
}
},
}

The Layout is transformed from a server-side component to a client-side component. The type field is changed from a direct import of Layout component to a module.reference. Meanwhile, its child components remain unchanged.

Before we change Layout to the Client component, the process of rendering Layout happens completely on the server side, and its children are also rendered on the server side. The rendered results are sent to the browser to be rendered as actual DOM elements.

After we change Layout to a client component, the rendering process Layout happens in the browser, but its child components are still rendered on the server side. When the browser renders the server’s JSON UI output, Layout inserts the results of the server-side child components into the browser DOM.

Since the props of the child components are not changed when user changes the layout width on the client side (because they have no props), the rendering result on the server side does not need to be recaptured.

Therefore, the answer to this question is “It can be converted to a client-side component, and the child components will not be recaptured.”

We can rewrite server-side components as client-side components in RSC projects without rewriting component composition at the use site. We can call this interchangeability “commutative” server/client components.

Conclusion

The documentation and RFC for React Server Component is relatively obscure and does not give practical examples, leading many people to wonder what it really is.

In this article, I tried to explain the design ideas and principles of React Server Component by explaining it with Dan’s quizzes.

It can help you understand this new feature and become one of the few materials to let you learn RSC without watching Youtube or following Twitter threads. I hope this will let you understand more about the principle of RSC and the three performance characteristics! Complete rendering, consistent state, and commutative server/client components.

It is not easy to create. If you think this article is helpful to you, please leave a comment or read more of my articles on Medium.

Reference

  1. React 18: React Server Components | Next.js
  2. What you need to know about React Server Components
  3. React Server Components. It’s not server-side rendering | by Nathan
  4. What are React Server Components? — FreeCodecamp
  5. How React Server Components Work
  6. 45% failed Dan’s Server Component Quiz

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Herrington Darkholme
Herrington Darkholme

Written by Herrington Darkholme

🌐 Frontend Vimmer, ⚒️OSS with @typescript @vuejs and @rustlang 💻 Previously worked at @BytedanceTalk. Hobby project: https://ast-grep.github.io/

Responses (1)