How React Server Components work
React Server Components (RSC) enable server-side component execution with client-side streaming. This document explains the underlying mechanisms and technical details of how RSC works under the hood.
Bundling Process
We showed in the Create a React Server Component without SSR article how to bundle React Server Components. During bundling, we used:
RSC Webpack Loader
The react-on-rails-rsc/WebpackLoader
is a custom loader that removes the client components and their dependencies from the RSC bundle and replace them with client references that tell the rsc runtime that there is a client component entry point in this place.
The RSC Webpack Loader works when it finds a file with the 'use client'
directive on top of it.
// app/javascript/client/app/components/HomePage.jsx
'use client';
import Footer from './Footer';
export const Header = () => {
return <div>Header</div>;
};
export default function HomePage() {
return (
<div>
<Header />
<div>Home Page</div>
<Footer />
</div>
);
};
It replaces all exports of the file with the client references.
NOTE
The code shown below represents internal implementation details of how React Server Components work under the hood. You don't need to understand these details to use React Server Components effectively in your application. This section is included for those interested in the technical implementation.
import { registerClientReference } from "react-server-dom-webpack/server";
export const Header = registerClientReference(
function () {
throw new Error(
"Attempted to call Header() from the server but Header is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component."
);
},
"file:///path/to/src/HomePage.jsx",
"Header"
);
export default registerClientReference(
function() {
throw new Error("Attempted to call the default export of file:///path/to/src/HomePage.jsx from the serverbut it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of aClient Component.");
},
"file:///path/to/src/HomePage.jsx",
"default"
);
When a file is marked with 'use client'
, the RSC Webpack Loader replaces all component exports with ClientReference
objects. The registerClientReference
function takes three arguments:
- A function that throws an error if someone tries to call the component function directly instead of rendering it as a component
- A string representing the file path of the client component, which serves as part of its unique identifier
- A string with the export name ("default" for default exports, or the named export identifier)
The second and third arguments are used to identify the client component when it needs to be hydrated in the browser.
Note that all imports from the original file are removed in the transformed code. This includes both the Footer
component import and any other dependencies that the client components may have used. The client component implementations and their dependencies are removed from the RSC bundle.
RSC Client Plugin
We also used react-on-rails-rsc/WebpackPlugin
with the client bundle. It does the following:
- Adds all files with the
'use client'
directive on top of it as entry points to the client bundle. - Creates the
react-client-manifest.json
file that contains the mapping of the client components files to their corresponding webpack chunk IDs.
Let's examine the react-client-manifest.json
file.
NOTE
The code shown below represents internal implementation details of how React Server Components work under the hood. You don't need to understand these details to use React Server Components effectively in your application. This section is included for those interested in the technical implementation.
First, you need to build the client bundle by running:
CLIENT_BUNDLE_ONLY=true bin/shakapacker
NOTE
When you run bin/dev
, the client bundle may not be written to the disk, it's served from the webpack-dev-server. That's why you need to run CLIENT_BUNDLE_ONLY=true bin/shakapacker
to ensure the client bundle is built and written to the disk.
Then, you can find the react-client-manifest.json
file in the public/webpack/development
or public/webpack/production
directory, depending on the environment you are building for.
Let's search for the client component ToggleContainer
that we built before in Add Stream and Interactivity to RSC Page article. You will find the following entry in the react-client-manifest.json
file:
"file:///path/to/app/javascript/components/ToggleContainer.jsx": {
"id": "./app/javascript/components/ToggleContainer.jsx",
"chunks": [
"client25",
"js/client25.js"
],
"name": "*"
},
This entry indicates that the ToggleContainer
client component is included in the client25
chunk. The js/client25.js
file contains the client-side code for the ToggleContainer
component. You can find the client25
chunk in the public/webpack/<environment>/js/client25.js
file. Also, the id
field is the Webpack module ID for the ToggleContainer
client component. It's used by react runtime to load and hydrate the component in the browser.
If you want to change the file name of the react-client-manifest.json
file, you can do so by setting the clientManifestFilename
option in the react-on-rails-rsc/WebpackPlugin
plugin as follows:
const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin');
config.plugins.push(new RSCWebpackPlugin({
isServer: false,
clientManifestFilename: 'client-components-webpack-manifest.json',
}));
And because React on Rails Pro uploads the react-client-manifest.json
file to the renderer while uploading the server bundle and it expects it to be named react-client-manifest.json
, you need to tell React on Rails Pro that the name is changed to client-components-webpack-manifest.json
.
# config/initializers/react_on_rails.rb
ReactOnRails.configure do |config|
config.react_client_manifest_file = "client-components-webpack-manifest.json"
end
React Server Component Payload (RSC Payload)
The React Server Component Payload (RSC Payload) is a mechanism that allows you to pass the rendered server components from the server to the client. You can use the rsc_payload_react_component
helper function to embed the RSC payload of any component in your Rails views. Let's try to embed the RSC payload of the ReactServerComponentPage
component in the app/views/pages/react_server_component_page_rsc_payload.html.erb
view.
<%= rsc_payload_react_component("ReactServerComponentPage") %>
Add the route to the app/config/routes.rb
file.
# config/routes.rb
get "/react_server_component_page_rsc_payload", to: "pages#react_server_component_page_rsc_payload"
And render the view using the stream_view_containing_react_components
helper method.
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
include ReactOnRailsPro::Stream
def react_server_component_page_rsc_payload
stream_view_containing_react_components(template: "pages/react_server_component_page_rsc_payload")
end
end
When you navigate to the http://localhost:3000/react_server_component_page_rsc_payload
page, you will see the RSC payload of the ReactServerComponentPage
component. You will find multiple JSON objects in the response body. Each represents a chunk of the RSC payload.
{
"html":"<RSC Payload>",
"consoleReplayScript":"",
"hasErrors":false,
"isShellReady":true
}
{
"html":"<RSC Payload>",
"consoleReplayScript":"",
"hasErrors":false,
"isShellReady":true
}
The real RSC payload is embedded in the html
field. Other fields are used by React on Rails Pro to ensure the RSC payload is rendered correctly and to replay the console logs in the browser.
NOTE
using html
field to refer to the RSC payload may be confusing. It will be changed later to rscPayload
, but it's an implementation detail and you should not rely on it.
The RSC payload itself is an implementation detail, you don't need to understand it to use React Server Components. But we can notice that it contains the React render tree of the ReactServerComponentPage
component. Like this:
1:["$","div",null,{"className":"server-component-demo","children":[["$","h2",null,{"children":"React Server Component Demo"}],["$","section",null,{"children":[["$","h3",null,{"children":"Date Calculations (using moment.js)"}]]}]]}]
The interesting part is how the RSC payload references the client components. Let's take a look at how it references the ToggleContainer
client component.
7:I["./app/javascript/components/ToggleContainer.jsx",["client25","js/client25.js"],"default"]
The RSC payload references client components by including:
- The webpack module ID of the client component (e.g. "./app/javascript/components/ToggleContainer.jsx")
- The webpack chunk IDs that contain the component code (e.g. ["client25","js/client25.js"])
- The export name being referenced (e.g. "default")
This information comes from the react-client-manifest.json
file, which maps client component paths to their corresponding webpack module and chunk IDs. That's why we needed to upload the react-client-manifest.json
file to the renderer as it's needed to generate the RSC payload.
Automatically Generate the RSC Payload
Usually, you don't need to generate the RSC payload manually. You can use the rsc_payload_route
helper method inside the config/routes.rb
file to automatically add the rsc route that accepts the component name as a parameter and returns the RSC payload.
# config/routes.rb
Rails.application.routes.draw do
rsc_payload_route
end
You can change the path of the rsc route by passing the path
option to the rsc_payload_route
method.
# config/routes.rb
Rails.application.routes.draw do
rsc_payload_route path: "/flight-payload"
end
In this case, ensure you pass the correct path to registerServerComponent
function in the client bundle.
// client/app/packs/client-bundle.js
import registerServerComponent from 'react-on-rails/registerServerComponent/client';
registerServerComponent({
rscPayloadGenerationUrlPath: "flight-payload",
}, "ReactServerComponentPage")
Or if you enabled the auto_load_bundle
option to make React on Rails automatically register react components, you can pass the path to the rsc_payload_generation_url_path
config in React on Rails Pro configuration.
# config/initializers/react_on_rails.rb
ReactOnRailsPro.configure do |config|
config.rsc_payload_generation_url_path = "flight-payload"
end
Next Steps
To learn more about how React Server Components are rendered in React on Rails Pro, including the rendering flow, bundle types, and upcoming improvements, see React Server Components Rendering Flow.