-
Notifications
You must be signed in to change notification settings - Fork 44
feat: add rspack rsc example #440
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| todos.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # Rspack React Server Components Example | ||
|
|
||
| This example is a server-driven app built with Rspack and React Server Components (RSC). In this setup, routing happens on the server, delivering HTML on initial page load, and client side rendering on subsequent navigations. It also demonstrates React Server Actions to perform mutations, both by calling as a function and as the target of an HTML form. | ||
|
|
||
| ## Setup | ||
|
|
||
| The example consists of the following main files: | ||
|
|
||
| ### server.js | ||
|
|
||
| This is the development server setup using Express, Rspack middleware and webpack-hot-middleware for HMR. It configures two Rspack compilation targets: one for the client bundle (web target) and one for the RSC server bundle (node target). The server imports the compiled RSC entry module and delegates requests to it. | ||
|
|
||
| The Rspack configuration that defines three build targets: | ||
|
|
||
| 1. **Client bundle** (web target): Compiles `src/framework/entry.client.tsx` with React Refresh and HMR support | ||
| 2. **RSC server bundle** (node target): Compiles `src/framework/entry.rsc.tsx` with RSC layer support using `rspack.experiments.rsc` plugins | ||
| 3. Both configurations use `builtin:swc-loader` with `rspackExperiments.reactServerComponents: true` to enable RSC support | ||
|
|
||
| The RSC configuration uses layers (`Layers.rsc` and `Layers.ssr`) to differentiate between server component code and SSR code, with appropriate resolve conditions (`react-server`) for RSC modules. | ||
|
|
||
| ### src/Todos.tsx | ||
|
|
||
| This is the entry React Server Component that renders the root `<html>` element, server content, and any client components. It is marked with the `"use server-entry"` directive, which indicates this is an entry point for the server component tree. | ||
|
|
||
| ### src/framework/entry.client.tsx | ||
|
|
||
| This is the main client entrypoint that hydrates the initial page and handles client-side routing. It uses `react-server-dom-rspack/client.browser` to deserialize RSC payloads into React VDOM. The client intercepts navigation events (via `popstate` and `history.pushState`) and re-fetches RSC payloads for client-side transitions. It also registers a server callback using `setServerCallback` to handle server action calls. | ||
|
|
||
| ### src/actions.ts | ||
|
|
||
| This is a server actions file. Functions exported by this file can be imported from the client and called to send data to the server for processing. It is marked using the `"use server"` directive. Rspack's RSC plugin detects this directive and places these actions into the server bundle while creating proxy modules on the client that communicate with the server via the handler registered in `entry.client.tsx`. | ||
|
|
||
| Currently, server actions must be defined in a separate file. Inline server actions (e.g. `"use server"` inside a function) are not yet supported. | ||
|
|
||
| ### src/framework/entry.rsc.tsx | ||
|
|
||
| This module handles RSC rendering and server action execution on the server using `react-server-dom-rspack/server.node`. It exports a request handler that: | ||
| - Differentiates between RSC fetch requests, SSR requests, and action calls | ||
| - Handles server actions by decoding the request and executing the action | ||
| - Renders the React tree to an RSC stream using `renderToReadableStream` | ||
| - Delegates to SSR for initial HTML rendering or returns raw RSC payload for client-side navigation | ||
|
|
||
| ### src/framework/entry.ssr.tsx | ||
|
|
||
| This module performs server-side rendering (SSR) of React components. It receives an RSC stream, deserializes it back into React VDOM using `createFromReadableStream` from `react-server-dom-rspack/client`, then renders it to HTML using `react-dom/server`. The RSC payload is also injected into the HTML as a script tag for client-side hydration using `rsc-html-stream`. | ||
|
|
||
| ### src/TodoItem.tsx and src/Dialog.tsx | ||
|
|
||
| These are client components. `<TodoItem>` renders a todo list item, and uses server actions and `useOptimistic` to implement the checkbox and remove buttons. `Dialog.tsx` renders a dialog component using client APIs, and accepts the create todo form (which is a server component) as children. | ||
|
|
||
| ## Initial HTML rendering | ||
|
|
||
| The flow of initial rendering starts on the server. | ||
|
|
||
| ### Server | ||
|
|
||
| The server uses Express to handle routing. When a route handler is called, it invokes the handler from `entry.rsc.tsx` which: | ||
|
|
||
| 1. Parses the request to determine if it's an RSC fetch, action call, or initial HTML request | ||
| 2. Renders the React component tree to an RSC stream using `renderToReadableStream` from `react-server-dom-rspack/server.node` | ||
| 3. For initial HTML requests, delegates to `entry.ssr.tsx` which: | ||
| - Deserializes the RSC stream back into React VDOM using `createFromReadableStream` from `react-server-dom-rspack/client` | ||
| - Renders the VDOM to HTML using `react-dom/server` | ||
| - Injects the RSC payload into the HTML stream as a script tag for client hydration | ||
|
|
||
| This approach allows the same RSC stream to be used for both SSR and client hydration, reducing redundant work. | ||
|
|
||
| ### Client | ||
|
|
||
| To hydrate the initial page, the client calls `createFromReadableStream` from `react-server-dom-rspack/client.browser` to deserialize the RSC payload embedded in the initial HTML (via `rsc-html-stream`). The deserialized payload is then hydrated using `hydrateRoot` from `react-dom/client`, making the page interactive. | ||
|
|
||
| ## Client side routing | ||
|
|
||
| The client includes a simple router in `entry.client.tsx`, allowing subsequent navigations after the initial page load to maintain client state without reloading the full HTML page. | ||
|
|
||
| ### Client | ||
|
|
||
| The client listens for navigation events using `popstate` (browser back/forward buttons) and intercepts `history.pushState` calls. To perform a navigation: | ||
|
|
||
| 1. Call `createFromFetch` from `react-server-dom-rspack/client.browser` to fetch a new RSC payload from the server with the appropriate `Accept` header | ||
| 2. Update the component state with the new payload, triggering a React transition to re-render the page | ||
| 3. Push the new URL to the browser's history if needed | ||
|
|
||
| These steps can be customized as needed for your server setup, e.g. using a more sophisticated client side router, or adding authentication headers. | ||
|
|
||
| ### Server | ||
|
|
||
| The server handles fetch requests for RSC payloads using the same request handler in `entry.rsc.tsx`. When the request is identified as an RSC fetch (based on request headers), `renderToReadableStream` serializes the component tree into an RSC payload and returns it with the `text/x-component` content type, skipping the SSR step. | ||
|
|
||
| ## Server actions | ||
|
|
||
| Server actions allow the client to call the server to perform mutations and other actions. There are two ways server actions can be called: by calling an action function from the client, or by submitting an HTML form (progressive enhancement). | ||
|
|
||
| ### Client | ||
|
|
||
| When a server action is called, the client sends a request to the server using the `callServer` callback registered via `setServerCallback` in `entry.client.tsx`. When a server action proxy function generated by Rspack is called on the client, this handler will be invoked with the id of the action and the arguments to pass to it. | ||
|
|
||
| 1. Create a request object with the action ID and encode the arguments using `encodeReply` from `react-server-dom-rspack/client.browser` | ||
| 2. Call `createFromFetch` to fetch the response, which includes both the new component tree and the return value of the server action | ||
| 3. Update the page state with the returned payload, triggering a re-render | ||
| 4. Extract and return the result of the server action from the payload | ||
|
|
||
| These steps can be customized as needed for your server setup, e.g. adding authentication headers. | ||
|
|
||
| ### Server | ||
|
|
||
| When a server action request is received in `entry.rsc.tsx`, the server performs the following steps: | ||
|
|
||
| 1. Parse the request to extract the action ID and decode the arguments using `decodeReply` from `react-server-dom-rspack/server.node` | ||
| 2. Load and execute the server action using `loadServerAction` | ||
| 3. Capture the return value (or error) from the action | ||
| 4. Render the component tree to an RSC payload using `renderToReadableStream`, including the action return value in the response | ||
| 5. Return the RSC payload to the client | ||
|
|
||
| For progressive enhancement (form submissions before JavaScript loads), the server decodes the form data using `decodeAction` and `decodeFormState`, executes the action, and returns the form state in the RSC payload for proper hydration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "rspack-rsc", | ||
| "private": true, | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "NODE_ENV=production rspack build", | ||
| "dev": "NODE_ENV=development NO_CSP=true node server.js" | ||
| }, | ||
| "devDependencies": { | ||
| "@rspack/cli": "2.0.0-rc.3", | ||
| "@rspack/core": "2.0.0-rc.3", | ||
| "@rspack/dev-server": "2.0.0-rc.3", | ||
| "@rspack/plugin-react-refresh": "^1.5.3", | ||
| "@types/express": "^5.0.6", | ||
| "@types/ws": "^8.18.1", | ||
| "css-loader": "^7.1.2", | ||
| "react-refresh": "^0.18.0", | ||
| "react-server-dom-rspack": "0.0.1-alpha.10", | ||
| "run-script-webpack-plugin": "^0.2.3", | ||
| "typescript": "^5.9.3", | ||
| "webpack-dev-middleware": "^7.4.5", | ||
|
SyMind marked this conversation as resolved.
|
||
| "webpack-hot-middleware": "^2.26.1", | ||
| "ws": "^8.19.0" | ||
| }, | ||
| "dependencies": { | ||
| "@types/node": "^24.10.1", | ||
| "@types/react": "^19.2.6", | ||
| "@types/react-dom": "^19.2.3", | ||
| "express": "^5.1.0", | ||
| "react": "^19.2.0", | ||
| "react-dom": "^19.2.0", | ||
| "rsc-html-stream": "^0.0.7", | ||
| "srvx": "^0.10.1" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import rspack from '@rspack/core'; | ||
| import ReactRefreshPlugin from '@rspack/plugin-react-refresh'; | ||
| import path from 'path'; | ||
|
|
||
| // Target browsers, see: https://github.com/browserslist/browserslist | ||
| const browserTargets = ['last 2 versions', '> 0.2%', 'not dead', 'Firefox ESR']; | ||
| // Target Node.js LTS version for server bundle | ||
| const nodeTargets = ['node 22']; | ||
|
|
||
| function jsRule(targets) { | ||
| return { | ||
| test: /\.jsx?$/, | ||
| use: [ | ||
| { | ||
| loader: 'builtin:swc-loader', | ||
| options: { | ||
| jsc: { | ||
| parser: { | ||
| syntax: 'ecmascript', | ||
| jsx: true, | ||
| }, | ||
| transform: { | ||
| react: { | ||
| runtime: 'automatic', | ||
| }, | ||
| }, | ||
| experimental: { | ||
| keepImportAttributes: true, | ||
| }, | ||
| }, | ||
| env: { targets }, | ||
| rspackExperiments: { | ||
| reactServerComponents: true, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| function tsRule(targets) { | ||
| return { | ||
| test: /\.tsx?$/, | ||
| use: [ | ||
| { | ||
| loader: 'builtin:swc-loader', | ||
| options: { | ||
| jsc: { | ||
| parser: { | ||
| syntax: 'typescript', | ||
| tsx: true, | ||
| }, | ||
| transform: { | ||
| react: { | ||
| runtime: 'automatic', | ||
| }, | ||
| }, | ||
| experimental: { | ||
| keepImportAttributes: true, | ||
| }, | ||
| }, | ||
| env: { targets }, | ||
| rspackExperiments: { | ||
| reactServerComponents: true, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| function cssRule() { | ||
| return { | ||
| test: /\.css$/i, | ||
| type: 'css/auto', | ||
| }; | ||
| } | ||
|
|
||
| const { createPlugins, Layers } = rspack.experiments.rsc; | ||
|
|
||
| export function createRspackConfig({ | ||
| hmr = false, | ||
| mode = process.env.NODE_ENV === 'development' ? 'development' : 'production', | ||
| onServerComponentChanges, | ||
| } = {}) { | ||
| const { ServerPlugin, ClientPlugin } = createPlugins(); | ||
| const ssrEntry = path.resolve(import.meta.dirname, 'src/framework/entry.ssr.tsx'); | ||
| const rscEntry = path.resolve(import.meta.dirname, 'src/framework/entry.rsc.tsx'); | ||
|
|
||
| return [ | ||
| { | ||
| name: 'client', | ||
| mode, | ||
| target: 'web', | ||
| context: import.meta.dirname, | ||
| entry: './src/framework/entry.client.tsx', | ||
| resolve: { | ||
| extensions: ['...', '.ts', '.tsx', '.jsx'], | ||
| }, | ||
| output: { | ||
| path: path.join(import.meta.dirname, 'dist/static'), | ||
| publicPath: 'static/', | ||
| }, | ||
| devtool: mode === 'development' ? 'source-map' : false, | ||
| module: { | ||
| rules: [cssRule(), jsRule(browserTargets), tsRule(browserTargets)], | ||
| }, | ||
| plugins: [ | ||
| new ClientPlugin(), | ||
| ...(hmr ? [new rspack.HotModuleReplacementPlugin(), new ReactRefreshPlugin()] : []), | ||
| ], | ||
| }, | ||
| { | ||
| name: 'server', | ||
| mode, | ||
| target: 'node', | ||
| context: import.meta.dirname, | ||
| entry: './src/framework/entry.rsc.tsx', | ||
| resolve: { | ||
| extensions: ['...', '.ts', '.tsx', '.jsx'], | ||
| }, | ||
| output: { | ||
| path: path.join(import.meta.dirname, 'dist'), | ||
| module: true, | ||
| chunkFormat: 'module', | ||
| chunkLoading: 'import', | ||
| library: { | ||
| type: 'module', | ||
| }, | ||
| }, | ||
| devtool: false, | ||
| module: { | ||
| rules: [ | ||
| cssRule(), | ||
| jsRule(nodeTargets), | ||
| tsRule(nodeTargets), | ||
| // react server components layers | ||
| { | ||
| resource: ssrEntry, | ||
| layer: Layers.ssr, | ||
| }, | ||
| { | ||
| resource: rscEntry, | ||
| layer: Layers.rsc, | ||
| resolve: { | ||
| conditionNames: ['react-server', '...'], | ||
| }, | ||
| }, | ||
| { | ||
| issuerLayer: Layers.rsc, | ||
| exclude: ssrEntry, | ||
| resolve: { | ||
| conditionNames: ['react-server', '...'], | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| plugins: [ | ||
| new ServerPlugin({ | ||
| onServerComponentChanges, | ||
| }), | ||
| ], | ||
| externalsType: 'module', | ||
| externals: { | ||
| express: 'express', | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| export default createRspackConfig(); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.