Understanding React Server Components

Authors
Tomasz Janiczek
Software Engineer
@
Callstack

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 'use client' 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.

import { format } from 'date-fns';
function MyComponent = () => {
const res = await fetch('http://big.data/very/large/dataset');
const data = await res.json();
return (<div>
<h1>{format(data.updatedAt, "'Updated on' eeee")}</h1>
</div>);
}
view raw RSC1.javascript hosted with ❤ by GitHub

If MyComponent 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:

[$1, 'div', {}, $2]
[$2, 'h1', 'Updated on Friday', null]
view raw RSC2.javascript hosted with ❤ by GitHub

If MyComponent is client, then as usual, the component definition will be sent along with part of date-fns 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 ($1), but developers have the ability to specify in what order the UI will be painted via Suspense.

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 Suspense. 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 if 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 useState 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:

function App() {
return <RSCRoot>
<ClientComponent>
<RSComponent />
</ClientComponent>
</RSCRoot>
}
view raw RSC3.javascript hosted with ❤ by GitHub

Passing data between them may be problematic. In this example, the RSC-enabled framework will:

  1. Stream RSCRoot markup to browser
  2. Stream RSComponent markup to browser
  3. Send ClientComponent definition to browser and run as soon as available
  4. When the RSCRoot is rendered will try to put result of ClientComponent inside

For server -> client pass data in props. The data must be serializable, so you can't pass:

  • function definitions
  • objects with circular references
  • promises
  • Map, Set, WeakMap, WeakSet
  • binary data (ArrayBuffer, Blob)
  • NaN

For client -> server 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 callServer 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.

function App() {
return (
<RSCRoot>
<RSCFClientInfo>
<ClientPanel>
<RSCProductInfo />
</ClientPanel>
</RSCClientInfo>
</RSCRoot>
);
}
view raw RSC4.javascript hosted with ❤ by GitHub

In the above example, there are two async components - RSCFClientInfo that fetches client data and RSCProductInfo 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 RSCProductInfo will not wait for ClientPanel to render before initializing the fetch. In the best case scenario - it may have the data already available before the browser manages to render ClientPanel, and all content will be displayed in one clean sweep.

There are edge cases to consider as well. In this example:

function App() {
return (
<RSCRoot>
<ClientGeoSwitch
usersInUS={<RSCLandingPageUS />}
usersInEurope={<RSCLandingPageEurope />}
/>
</RSCRoot>
);
}
view raw RSC5.javascript hosted with ❤ by GitHub

The developer may want to check the user’s geolocation data from the browser and then render either RSCLandingPageUS or RSCLandingPageEurope conditionally. The tricky part is that since there’s a single pass on the server - React will have to evaluate both RSCLandingPageUS and RSCLandingPageEurope. 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:

  1. 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.
    ‍
  2. 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!

Insights

Learn more about React

cover

Jun 24Article

Writing Custom Renderers for React

Learn how React renderers work, why RNTL needed a new test renderer, and what it takes to connect React Reconciler to a host environment.

cover

Dec 18Live Stream

Building iOS Live Activities With React: Inside Voltra

Learn how to build iOS Live Activities and Dynamic Island experiences using pure JSX, no Swift or native code required.

cover

Nov 27Podcast

From React Native Web to React Strict DOM

Nicolas Gallagher (Meta) explains how React Strict DOM evolved from React Native Web, providing a shared syntax that compiles to native or web with zero overhead.

cover

Nov 21Podcast

React Foundation: New Governance Model for React and React Native

Seth Webster, Executive Director of the React Foundation, details its new governance model, Meta's role, and how partners like Callstack and Microsoft will ensure stability and scale.

cover

Apr 9Podcast

Exploring TanStack Ecosystem: From React Query to TanStack Start

Tanner Linsley breaks down TanStack’s evolution-from React Query to Start, and why client-first React apps still matter in today’s full-stack landscape.

Event Cover

Feb 20Live Stream

Create React App is Out: Exploring Alternatives & Community Proposals

CRA is now deprecated, so we explore other options for starting web React projects, going through Vite, TanStack Router, Parcel, and Rsbuild to check the developer experience.

Event Cover

Feb 13Live Stream

State of React 2024 Breakdown

Let's take a deep dive into the long-awaited State of React survey from 2024; brace yourselves for a two-hour ride.

cover

Sep 9Article

The Complete Developer Guide to React 19, Part 3: Upgraded Support

Learn how React 19's new features enhance SEO, page performance, and custom elements support for better, faster web applications.

cover

Sep 3Article

The Complete Developer Guide to React 19, Part 2: New Enhancements

Discover React 19's key updates, including improved `ref` usage, error reporting, and context consumption in part 2 of this practical developer guide.

cover

Sep 2Article

The Complete Developer Guide to React 19, Part 1: Async Handling

Explore the key features of React 19 beta, focusing on new hooks that enhance async handling, state management, and form submissions.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.