The Module Problem at the Heart of RSC

React Server Components treats a server/client application as one program expressed across two runtimes. RSC’s implementation has two core pieces: packages/react-server, which serializes React trees, and packages/react-client, which deserializes them. Both live in the React repo and are open source, but neither gets published to npm in raw form. The missing ingredient is module system integration.

Most serializers only ship data. RSC also ships code. Rendering a <p> tag to JSON is straightforward, but what happens when the tree contains a component?

<p>Hello, world</p>

That is easy to turn into JSON:

{
  type: 'p',
  props: {
    children: 'Hello world'
  }
}

A <Counter> component is different:

import { Counter } from './client';
 
<Counter initialCount={10} />
'use client';
 
import { useState, useEffect } from 'react';
 
export function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // ...
}

The goal is not a snapshot of the component—it’s the component itself, with all its logic, revived on the client. Embedding the source text in the payload would mean shipping strings to eval, and repeating the same component’s code in every response. A better assumption is that the code is already served as static JS, and the payload can simply reference it, much like a <script> tag:

{
  type: '/src/client.js#Counter', // "Load src/client.js and grab Counter"
  props: {
    initialCount: 10
  }
}

Yet loading modules one by one from source files over the network would create an import waterfall. The client cannot know the import graph ahead of time. The established answer—used for two decades in client-side work—is bundling.

How Bundler Bindings Work

A bundler is not strictly required. React includes a bundler-less ESM proof of concept, but it exists mostly as a demonstration because the naive approach is inefficient without further optimizations.

Realistic RSC integrations are bundler-specific. Bindings for Parcel and Webpack live in the React repo, with a Vite binding in progress. They handle modules in three phases:

  • During the build, the bindings locate files marked with 'use client' and create bundle chunks for those entry points—similar to Astro Islands.
  • On the server, they tell React how to reference modules in serialized output—for instance, a reference like 'chunk123.js#Counter'.
  • On the client, they teach React to ask the bundler runtime to load those modules. The Parcel bindings, for example, call a Parcel-specific function for this.

The server-side API for serializing a tree is exposed through these bindings:

import { serialize } from 'react-server-dom-yourbundler'; // Bundler-specific package
 
const reactTree = <Counter initialCount={10} />;
const outputString = serialize(reactTree); // Something like the JSON above

The resulting outputString can be stored, sent, or cached, and later passed to the React Client, which deserializes the whole tree and loads code from referenced modules as needed:

import { deserialize } from 'react-server-dom-yourbundler/client';  // Bundler-specific package
 
const outputString = // ... received over network, read from disk, etc...
const reactTree = deserialize(outputString); // <Counter initialCount={10} />

If everything lines up, the result is ordinary JSX, as if you had written <Counter initialCount={10} /> directly on the client. The tree can be rendered, stored in state, or converted to HTML.

const outputString = // ... received over network, read from disk, etc...
const reactTree = deserialize(outputString); // <Counter initialCount={10} />
 
// You can do anything you'd do with a regular JSX tree, for example:
const root = createRoot(domNode);
root.render(reactTree);

These low-level APIs are what RSC frameworks like Next.js use under the hood. For experimentation at this level, the Parcel RSC implementation is a practical starting point.

The serialize and deserialize names above are illustrative—bindings choose their own names and may offer multiple overloads. The @parcel/rsc package, a thin wrapper over react-server-dom-parcel, exposes serialization as renderRSC and deserialization as fetchRSC. Those implementations are non-blocking and support streaming in both directions.