Skip to content

Add support for SSR when the server bundle is webpack bundled v19.1.0 #33540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

AbanoubGhadban
Copy link

React Server Components SSR with Webpack

Overview

This PR implements support for SSR of React Server Components (RSC) with Webpack bundling. The implementation focuses on properly handling client components in both server and client bundles, ensuring correct rendering and hydration while maintaining the benefits of RSC architecture.

Problem Statement

The current React Server Components implementation assumes direct access to original JS files on the server, which doesn't work well with Webpack-bundled applications. This creates issues when trying to render client components on the server side.

Technical Solution

Client Component Handling

When building the RSC bundle, React removes all client components and replaces them with client references. For example, a client component:

// app/components/MyClientComponent.jsx
export function MyClientComponent(props) {
  return <div>Hello from client component</div>;
}

is replaced with:

import { registerClientReference } from "react-on-rails-rsc/server";

export const MyClientComponent = registerClientReference(
  function() {
    throw new Error("Attempted to call the default export of file:///home/user/my-app/src/app/components/MyClientComponent.jsx from the server but 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 a Client Component.");
  },
  "file:///home/user/my-app/src/app/components/MyClientComponent.jsx",
  "MyClientComponent"
);

When the RSC bundle is used to generate the RSC payload, it renders the server
components as their code already exists in it, but it will leave a reference to
the client component in the places where the client component is used.

Client-Side Implementation

1. Client Bundle Building

  • The RSC plugin treats files with "use client" directive as entry points
  • Only entry points need the directive; imported files inherit client status
  • Client bundle consists of multiple chunks, each representing a client component
  • Chunking strategy may group related components or split them based on optimization

2. Manifest Generation

The RSC plugin generates a react-client-manifest.json file mapping client components to their webpack IDs:

{
  "file:///home/user/my-app/src/app/components/MyClientComponent.jsx": {
    "id": 877,
    "chunks": [
      701,
      "static/js/client0.0cd0d2f4.chunk.js"
    ],
    "name": "*"
  },
  "file:///home/user/my-app/src/app/components/Counter.jsx": {
    "id": 779,
    "chunks": [
      498,
      "static/js/client2.3f22a7a5.chunk.js"
    ],
    "name": "*"
  }
}

3. RSC Payload Generation

  • React runtime uses the manifest to find module and chunk IDs
  • Client references are embedded in the payload:
7:I["779",[498,"static/js/client2.3f22a7a5.chunk.js"],"default"]

4. Client-Side Rendering

  • Runtime checks if webpack module is loaded
  • Loads JS chunks from server if needed
  • Renders or hydrates client components

Server-Side Implementation

1. Current React Behavior

React assumes server has access to original JS files, generating react-ssr-manifest.json:

{
  "moduleLoading": {
    "prefix": "/webpack/production",
    "crossOrigin": null
  },
  "moduleMap": {
    "877": {
      "*": {
        "specifier": "file:///home/user/my-app/src/app/components/MyClientComponent.jsx",
        "name": "*"
      }
    },
    "779": {
      "*": {
        "specifier": "file:///home/user/my-app/src/app/components/Counter.jsx",
        "name": "*"
      }
    }
  }
}

So, when React runtime faces the following RSC payload chunk:

7:I["779",[498,"static/js/client2.3f22a7a5.chunk.js"],"default"]

It will look at the react-ssr-manifest.json file to find the original file path of the client component (in this case, it's file:///home/user/my-app/src/app/components/Counter.jsx). Then, it will load that file and get the client component code from it.

As we said before, the info generated by the RSC plugin is only valuable when the server is not using a webpack bundled bundle. However, React runtime supports SSR from webpack bundles. The problem is that the needed info is not provided by the webpack bundle.

2. Proposed Solution

We need a JSON file mapping client module IDs to server module IDs:

{
  "779": {
    "*": {
      "id": 877,
      "chunks": [
        701,
        "static/server-bundle.js"
      ],
      "name": "*"
    }
  }
}

The previous example maps the client module ID 779 to the server module ID
877 that contains the same client component code (in this case, it's file:/// home/user/my-app/src/app/components/Counter.jsx).

Implementation Steps

Development Setup

To implement these changes, we need to modify the react-server-dom-webpack package. Here's how we set up our development environment:

  1. Fork and clone the React repository from our fork

  2. Make the necessary changes to the react-server-dom-webpack package as outlined in this PR

  3. Build the package using the React build system

  4. Copy the built package to ./react-server-dom-webpack in our react-on-rails-rsc project

  5. RSC Plugin Configuration

    • Add isServer option to differentiate bundles
    • Generate appropriate manifest files based on bundle type
    • Remove react-ssr-manifest.json generation
  6. Manifest Generation

    • Client bundle: Generate react-client-manifest.json
    • Server bundle: Generate react-server-client-manifest.json
    • Merge manifests at runtime
  7. API Changes

    • Export buildClientRenderer instead of createFromNodeStream
    • Export buildServerRenderer instead of renderToPipeableStream
    • Cache merged manifest to prevent repeated processing

API Usage

import { buildClientRenderer } from 'react-on-rails-rsc/server.node';
import { buildServerRenderer } from 'react-on-rails-rsc/client.node';

// Client-side rendering
const { createFromNodeStream } = buildClientRenderer(clientManifest, serverManifest);

// Server-side rendering
const { renderToPipeableStream } = buildServerRenderer(clientManifest);

Design Decisions

  1. Manifest Merging

    • Merged at initialization to prevent repeated processing
    • Cached for better performance
    • Hidden from user API for cleaner interface
  2. API Design

    • Chose buildClientRenderer/buildServerRenderer over direct exports
    • Better handles future React Server Components changes
    • Avoids breaking changes in the future

Add server support for React Flight Webpack Plugin

make rsc plugin create a manifest for server build
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants