Transforming web development
Announced in 2020, React Server Components are a long time coming. With a long list of tools required to support them, we may still be a year or two away before they become ubiquitous. But even before they do, we can already say that React Server Components will bring about the biggest change in how we write web applications since the introduction of React back in 2013. With rapid developments in this area, now is the best time to invest in understanding the core concepts of future React.
Unfortunately, there are no official docs available. The reason for that is that there are a lot of moving parts still being figured out (which we will explore later), lots of decisions still to be made, and lots of edge cases still in need of proper triage. That said, core rules are already laid out, and React team does a great job passing them around via Twitter, various podcasts, and streams, including the most recent stream on RSC featuring Dan Abramov. Four-hour running time may be a bit too much to take in one go, so we prepared a concise summary of the stream, and add a couple of bits from other sources as well.
Paradigm shift
The naming of RSCs is a bit misleading, but React team couldn't find a better alternative. RSC doesn't mean components render on the server side.
For example, Next.js can render client components on the server side (in SSR). Also, in the future, it could be possible to render server components on the client side as well. The key idea here is that RSCs run ahead of time compared to the regular React rendering cycle.
When exactly do they run? That’s not specified. If we have an RSC that needs to read static files from the filesystem, it could run during the build time. If we have an RSC that needs to get fresh data from the DB, it could run upon request.
In the new paradigm, the UI will be built in two phases:
- First pass (server components) - render, then stream only the result,
- Second pass (client components) - send the definition, then render during runtime
and the components should be easily ported from one type to another.
RSCs are all rendered in a single pass upon request, yet may still be prepared ahead of time. This may be confusing at first. A good example to illustrate how this work is React’s patch for a native fetch() method that adds options for caching. The component fetches data upon the first request. When the same page is requested again, the cached data is used to render this particular RSC.
RSCs are not better than client components. They come with their own set of tradeoffs. Developers should be able to decide when is the best time to run a given component on a case-to-case scenario. The current solution to switching components between server/client is the <rte-code>'use client'<rte-code> directive.
How RSC works
RSC parses JSX, executes JS, and streams results to the client in small batches as soon as they become available.
Example: To display your UI you have to GET an endpoint that returns a very large JSON, but in the UI, you only use one property from that JSON.
If <rte-code>MyComponent<rte-code> is RSC, it will fetch on server, then parse your component and stream to the browser only markup containing this single property, something like this:
If <rte-code>MyComponent<rte-code> is client, then as usual, the component definition will be sent along with part of <rte-code>date-fns<rte-code> library. Then on each render the browser would fetch full dataset to display only the title.
The tradeoff here is that RSC will only send data that is used to display UI, but along that, it will send all the markup as well on each request. Usually, this is not a problem, but if there’s a lot of markup, you might be better off sending client component.
In RSC variant, the server sends only the resulting UI (component definition and other libraries are not transferred). Also, it streams the UI in small batches as soon as they are processed. Each loaded batch contains a small chunk of data + info on what else it is needed + in which chunk it will eventually come.
The client is ready to execute JS as soon as it receives the first chunk (<rte-code>$1<rte-code>), but developers have the ability to specify in what order the UI will be painted via <rte-code>Suspense.<rte-code>
Example: there's a component that imports a lot of CSS, which is slow to load. We can stream & start processing this component immediately but postpone painting it until CSS arrives with <rte-code>Suspense.<rte-code> This way when the CSS finishes loading, React already has the VDOM ready to go.
Problems RSC try to solve
Problems with client-only architecture and how RSCs are designed to mitigate them:
1. If you have an <rte-code>if<rte-code> statement, you have to ship both code paths to the client, even if the client uses only one of them.
- RSC will stream only the result of a single code path
- tradeoff: to display a different path, you have to invalidate the whole branch of the RSC tree (fortunately, it's not a full page refresh, so the client state is not lost there) - this means no <rte-code>useState<rte-code> in RSCs
2. Most of the time, you ship more data than client needs to display UI, even with GraphQL. The data sent from the server usually has to be additionally processed on the client before displaying.
- RSC will stream only the data used in UI
- tradeoff: RSC will stream component's markup each time it's needed
3. You need to download the whole bundle to display an SPA page, even if that particular page could be a static HTML.
- RSC will stream only markup required to display the requested view, no manual code splitting required
- tradeoff: after downloading the whole bundle to the client all transitions can be instant, where RSC architecture still have to call the server on navigation
4. Fetching data on demand produces waterfalls. Caching is also problematic.
- RSC may render all server views and start fetching all the data ASAP
- tradeoff: developers still should manually define when something is painted
Server - client boundary
Since the components are executed differently (most often even on different machines), it's not trivial to pass data between two types of components. Even though we can compose them interchangeably:
Passing data between them may be problematic. In this example, the RSC-enabled framework will:
- Stream <rte-code>RSCRoot<rte-code> markup to browser
- Stream <rte-code>RSComponent<rte-code> markup to browser
- Send <rte-code>ClientComponent<rte-code> definition to browser and run as soon as available
- When the <rte-code>RSCRoot<rte-code> is rendered will try to put result of <rte-code>ClientComponent<rte-code> inside
For <rte-code>server -> client<rte-code> pass data in props. The data must be serialisable, so you can't pass:
- function definitions
- objects with circular references
- promises
- Map, Set, WeakMap, WeakSet
- binary data (ArrayBuffer, Blob)
- NaN
For <rte-code>client -> server<rte-code> you can:
- change URL in JS and allow framework's router to invalidate RSC tree
- make HTTP request to server and invalidate RSC tree from the request handler
Limitations and workarounds for these approaches are being investigated, and new stuff shows up almost every week, like <rte-code>callServer<rte-code> function in Wakuwork.
Eager evaluation
RSC powered app starts with a single root on the server. From there React will create a map of components, marking them as server/client component. When the time comes to serve the app to the client - React will evaluate all RSCs in the tree immediately. This mitigates the problem with waterfalls, since all server-side async components can start their jobs upon request.
In the above example, there are two async components - <rte-code>RSCFClientInfo<rte-code> that fetches client data and <rte-code>RSCProductInfo<rte-code> that fetches product details. The important concept here is that the whole server-side tree of the app will be parsed in a single pass. This means that <rte-code>RSCProductInfo<rte-code> will not wait for <rte-code>ClientPanel<rte-code> to render before initializing the fetch. In the best case scenario - it may have the data already available before the browser manages to render <rte-code>ClientPanel<rte-code>, and all content will be displayed in one clean sweep.
There are edge cases to consider as well. In this example:
The developer may want to check the user’s geolocation data from the browser and then render either <rte-code>RSCLandingPageUS<rte-code> or <rte-code>RSCLandingPageEurope<rte-code> conditionally. The tricky part is that since there’s a single pass on the server - React will have to evaluate both <rte-code>RSCLandingPageUS<rte-code> and <rte-code>RSCLandingPageEurope<rte-code>. That means both components will fetch their respective data, templates for both components will be created and passed to the client.
Frameworks
Over time the RSC became an umbrella term for all the changes required to fulfil the vision of future React. Other pieces of the puzzle are, for example: a brand new code bundler, a server side router, server functions, a solution for data streaming. All these features are not trivial to build from scratch, that’s why there’s a need for full fledged framework to build future React apps. RSCs are meant to be abstracted by frameworks.
Will the Meta team provide such a framework? Well, no. Meta already has its own framework (Relay) but due to their very individual use case, it is not recommended for most apps. The core team decided to ask the Open Source community to provide their interpretations of the RSC vision. Currently, the Next.js /app router is the closest.
Some of the challenges are:
- Server-side router: The idea is that the router should not be a part of the client-side bundle. The router should live outside of React and provide users with mini-bundles containing only the stuff needed to display a particular page. This simplifies code splitting and reduces loading times.
- Code bundler: All current code bundlers are designed to produce client-only bundles. RSC-enabled bundler would have to:
- bootstrap server component root and other server components
- handle "holes" for client components
- code split client components to send them on demand
- figure out how to pass server props to these client components
Current state - as of May 2023
RSC are still pretty much work-in-progress. The best implementation so far is Next.js with their /app router and Turborepo. The second big player in React ecosystem, Remix - decided to pass on RSC for now due to tradeoffs. But still, they are most likely to adopt them in the future. With every day more and more people jump in and provide astonishing new ideas and solutions:
- Jarred Sumner (person behind Bun) promises experimental support in 0.6.0
- Daishi Kato (person behind Jotai) created minimalistic implementation
- Tanner Linsley (person behind react-query) commits to supporting RSC in his projects
- Angular and Vue are exploring this space as well. Qwik is a new promising framework (not based on React) having full server/client interoperability (Qwik even allows passing functions with closures over the client/server boundary!)
There’s still a long way ahead, but with all the sharpest minds in the industry embracing new paradigm the future is bright. What a time to be a developer!