Skip to content

Commit 08135de

Browse files
committed
feat: add rspack rsc example
1 parent 5096a4c commit 08135de

File tree

20 files changed

+2957
-30
lines changed

20 files changed

+2957
-30
lines changed

pnpm-lock.yaml

Lines changed: 1419 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rsbuild/ssr-express/src/index.server.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOMServer from 'react-dom/server';
33
import App from './App';
44

55
export function render() {
6+
console.log('sdfasdfsaf');
67
return ReactDOMServer.renderToString(
78
<React.StrictMode>
89
<App />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Local
2+
.DS_Store
3+
*.local
4+
*.log*
5+
6+
# Dist
7+
node_modules
8+
dist/
9+
10+
# Profile
11+
.rspack-profile-*/
12+
13+
# IDE
14+
.vscode/*
15+
!.vscode/extensions.json
16+
.idea
17+
18+
todos.json
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Rspack React Server Components Example
2+
3+
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.
4+
5+
## Setup
6+
7+
The example consists of the following main files:
8+
9+
### server.js
10+
11+
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.
12+
13+
The Rspack configuration that defines three build targets:
14+
15+
1. **Client bundle** (web target): Compiles `src/framework/entry.client.tsx` with React Refresh and HMR support
16+
2. **RSC server bundle** (node target): Compiles `src/framework/entry.rsc.tsx` with RSC layer support using `rspack.experiments.rsc` plugins
17+
3. Both configurations use `builtin:swc-loader` with `rspackExperiments.reactServerComponents: true` to enable RSC support
18+
19+
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.
20+
21+
### src/Todos.tsx
22+
23+
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.
24+
25+
### src/framework/entry.client.tsx
26+
27+
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.
28+
29+
### src/actions.ts
30+
31+
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`.
32+
33+
Currently, server actions must be defined in a separate file. Inline server actions (e.g. `"use server"` inside a function) are not yet supported.
34+
35+
### src/framework/entry.rsc.tsx
36+
37+
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:
38+
- Differentiates between RSC fetch requests, SSR requests, and action calls
39+
- Handles server actions by decoding the request and executing the action
40+
- Renders the React tree to an RSC stream using `renderToReadableStream`
41+
- Delegates to SSR for initial HTML rendering or returns raw RSC payload for client-side navigation
42+
43+
### src/framework/entry.ssr.tsx
44+
45+
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`.
46+
47+
### src/TodoItem.tsx and src/Dialog.tsx
48+
49+
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.
50+
51+
## Initial HTML rendering
52+
53+
The flow of initial rendering starts on the server.
54+
55+
### Server
56+
57+
The server uses Express to handle routing. When a route handler is called, it invokes the handler from `entry.rsc.tsx` which:
58+
59+
1. Parses the request to determine if it's an RSC fetch, action call, or initial HTML request
60+
2. Renders the React component tree to an RSC stream using `renderToReadableStream` from `react-server-dom-rspack/server.node`
61+
3. For initial HTML requests, delegates to `entry.ssr.tsx` which:
62+
- Deserializes the RSC stream back into React VDOM using `createFromReadableStream` from `react-server-dom-rspack/client`
63+
- Renders the VDOM to HTML using `react-dom/server`
64+
- Injects the RSC payload into the HTML stream as a script tag for client hydration
65+
66+
This approach allows the same RSC stream to be used for both SSR and client hydration, reducing redundant work.
67+
68+
### Client
69+
70+
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.
71+
72+
## Client side routing
73+
74+
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.
75+
76+
### Client
77+
78+
The client listens for navigation events using `popstate` (browser back/forward buttons) and intercepts `history.pushState` calls. To perform a navigation:
79+
80+
1. Call `createFromFetch` from `react-server-dom-rspack/client.browser` to fetch a new RSC payload from the server with the appropriate `Accept` header
81+
2. Update the component state with the new payload, triggering a React transition to re-render the page
82+
3. Push the new URL to the browser's history if needed
83+
84+
These steps can be customized as needed for your server setup, e.g. using a more sophisticated client side router, or adding authentication headers.
85+
86+
### Server
87+
88+
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.
89+
90+
## Server actions
91+
92+
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).
93+
94+
### Client
95+
96+
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.
97+
98+
1. Create a request object with the action ID and encode the arguments using `encodeReply` from `react-server-dom-rspack/client.browser`
99+
2. Call `createFromFetch` to fetch the response, which includes both the new component tree and the return value of the server action
100+
3. Update the page state with the returned payload, triggering a re-render
101+
4. Extract and return the result of the server action from the payload
102+
103+
These steps can be customized as needed for your server setup, e.g. adding authentication headers.
104+
105+
### Server
106+
107+
When a server action request is received in `entry.rsc.tsx`, the server performs the following steps:
108+
109+
1. Parse the request to extract the action ID and decode the arguments using `decodeReply` from `react-server-dom-rspack/server.node`
110+
2. Load and execute the server action using `loadServerAction`
111+
3. Capture the return value (or error) from the action
112+
4. Render the component tree to an RSC payload using `renderToReadableStream`, including the action return value in the response
113+
5. Return the RSC payload to the client
114+
115+
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.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "rspack-rsc-project",
3+
"private": true,
4+
"version": "1.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "NO_CSP=true node server.js"
8+
},
9+
"devDependencies": {
10+
"@rspack/cli": "2.0.0-beta.0",
11+
"@rspack/core": "2.0.0-beta.0",
12+
"@rspack/dev-server": "^1.2.1",
13+
"@rspack/plugin-react-refresh": "^1.5.3",
14+
"@types/express": "^5.0.6",
15+
"@types/ws": "^8.18.1",
16+
"css-loader": "^7.1.2",
17+
"react-refresh": "^0.18.0",
18+
"react-server-dom-rspack": "0.0.1-alpha.10",
19+
"run-script-webpack-plugin": "^0.2.3",
20+
"typescript": "^5.9.3",
21+
"webpack-dev-middleware": "^7.4.5",
22+
"webpack-hot-middleware": "^2.26.1",
23+
"ws": "^8.19.0"
24+
},
25+
"dependencies": {
26+
"@types/node": "^24.10.1",
27+
"@types/react": "^19.2.6",
28+
"@types/react-dom": "^19.2.3",
29+
"express": "^5.1.0",
30+
"react": "^19.2.0",
31+
"react-dom": "^19.2.0",
32+
"rsc-html-stream": "^0.0.7",
33+
"srvx": "^0.10.1"
34+
}
35+
}

0 commit comments

Comments
 (0)