Skip to content

fix(rsc): simplify RSC APIs #13883

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 4 commits into from
Jun 26, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/api/rsc/RSCHydratedRouter.md
Original file line number Diff line number Diff line change
@@ -12,9 +12,9 @@ Hydrates a server rendered `ServerPayload` in the browser.

## Props

### decode
### createFromReadableStream

Wraps your `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server.
Your `react-server-dom-xyz/client`'s `createFromReadableStream` function, used to decode payloads from the server.

### payload

8 changes: 4 additions & 4 deletions docs/api/rsc/createCallServer.md
Original file line number Diff line number Diff line change
@@ -12,10 +12,10 @@ Create a React `callServer` implementation for React Router.

## Options

### decode
### createFromReadableStream

Wraps your `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server.
Your `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server.

### encodeAction
### encodeReply

Wraps your `react-server-dom-xyz/client`'s `encodeReply`. Used when sending payloads to the server.
Your `react-server-dom-xyz/client`'s `encodeReply`. Used when sending payloads to the server.
12 changes: 8 additions & 4 deletions docs/api/rsc/matchRSCServerRequest.md
Original file line number Diff line number Diff line change
@@ -12,13 +12,13 @@ Matches the given routes to a Request and returns a RSC Response encoding a `Ser

## Options

### decodeCallServer
### decodeAction

A function responsible for loading a server function, using your `react-server-dom-xyz/server`'s `decodeReply` to decode the server function's arguments, and bind them to the implementation for invocation by the router.
Your `react-server-dom-xyz/server`'s `decodeAction` function, responsible for loading a server action.

### decodeFormAction
### decodeReply

A function responsible for loading a server action using your `react-server-dom-xyz/server`'s `decodeAction`.
Your `react-server-dom-xyz/server`'s `decodeReply` function, used to decode the server function's arguments and bind them to the implementation for invocation by the router.

### decodeFormState

@@ -28,6 +28,10 @@ A function responsible for decoding form state for progressively enhanceable for

A function responsible for using your `renderToReadableStream` to generate a Response encoding the `ServerPayload`.

### loadServerAction

Your `react-server-dom-xyz/server`'s `loadServerAction` function, used to load a server action by ID.

### request

The request to match against.
8 changes: 4 additions & 4 deletions docs/api/rsc/routeRSCServerRequest.md
Original file line number Diff line number Diff line change
@@ -12,13 +12,13 @@ Routes the incoming request to the RSC server and appropriately proxies the serv

## Options

### callServer
### createFromReadableStream

A function that forwards a `Request` to the RSC handler and returns a `Promise<Response>` containing a serialized `ServerPayload`.
Your `react-server-dom-xyz/client`'s `createFromReadableStream` function, used to decode payloads from the server.

### decode
### fetchServer

Wraps your `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server.
A function that forwards a `Request` to the RSC handler and returns a `Promise<Response>` containing a serialized `ServerPayload`.

### renderHTML

73 changes: 29 additions & 44 deletions docs/start/rsc/installation.md
Original file line number Diff line number Diff line change
@@ -74,34 +74,18 @@ import {
renderToReadableStream,
// @ts-expect-error - no types for this yet
} from "react-server-dom-parcel/server.edge";
import type {
unstable_DecodeCallServerFunction as DecodeCallServerFunction,
unstable_DecodeFormActionFunction as DecodeFormActionFunction,
} from "react-router";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";

// Import the prerender function from the client environment
import { prerender } from "./prerender" with { env: "react-client" };
import { routes } from "./routes/routes";

// Decode and load actions by ID to support post-hydration server actions.
const decodeCallServer: DecodeCallServerFunction = async (actionId, reply) => {
const args = await decodeReply(reply);
const action = await loadServerAction(actionId);
return action.bind(null, ...args);
};

// Decode and load actions by form data to pre-hydration server actions.
const decodeFormAction: DecodeFormActionFunction = async (formData) => {
return await decodeAction(formData);
};

function callServer(request: Request) {
function fetchServer(request: Request) {
return matchRSCServerRequest({
// Provide the React Server touchpoints.
decodeCallServer,
decodeFormAction,
decodeFormState,
decodeReply,
decodeAction,
loadServerAction,
// The incoming request.
request,
// The app routes.
@@ -140,7 +124,7 @@ app.use(
createRequestListener((request) =>
prerender(
request,
callServer,
fetchServer,
(routes as unknown as { bootstrapScript?: string }).bootstrapScript
)
)
@@ -167,16 +151,16 @@ import { createFromReadableStream } from "react-server-dom-parcel/client.edge";

export async function prerender(
request: Request,
callServer: (request: Request) => Promise<Response>,
fetchServer: (request: Request) => Promise<Response>,
bootstrapScriptContent: string | undefined
): Promise<Response> {
return await routeRSCServerRequest({
// The incoming request.
request,
// How to call the React Server.
callServer,
// How to fetch from the React Server.
fetchServer,
// Provide the React Server touchpoints.
decode: createFromReadableStream,
createFromReadableStream,
// Render the router to HTML.
async renderHTML(getPayload) {
return await renderHTMLToReadableStream(
@@ -199,8 +183,8 @@ Create a `src/browser.tsx` file that will act as the entrypoint for hydration.

import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import type { unstable_DecodeServerResponseFunction as DecodeServerResponseFunction } from "react-router";
import {
type unstable_ServerPayload as ServerPayload,
unstable_createCallServer as createCallServer,
unstable_getServerStream as getServerStream,
unstable_RSCHydratedRouter as RSCHydratedRouter,
@@ -212,31 +196,32 @@ import {
// @ts-expect-error - no types for this yet
} from "react-server-dom-parcel/client";

const decode: DecodeServerResponseFunction = (body) =>
createFromReadableStream(body);

// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
createCallServer({
decode,
encodeAction: (args) => encodeReply(args),
createFromReadableStream,
encodeReply,
})
);

// Get and decode the initial server payload
decode(getServerStream()).then((payload) => {
startTransition(async () => {
hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter
decode={decode}
payload={payload}
/>
</StrictMode>
);
});
});
createFromReadableStream(getServerStream()).then(
(payload: ServerPayload) => {
startTransition(async () => {
hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter
payload={payload}
createFromReadableStream={
createFromReadableStream
}
/>
</StrictMode>
);
});
}
);
```

## Define our Routes
2 changes: 1 addition & 1 deletion integration/helpers/rsc-parcel-framework/package.json
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@
"@types/react-dom": "^19.0.3",
"@types/react": "^19.0.8",
"parcel": "2.15.0",
"parcel-config-react-router-experimental": "1.0.22",
"parcel-config-react-router-experimental": "1.0.23",
"typescript": "^5.1.6"
},
"dependencies": {
16 changes: 8 additions & 8 deletions integration/helpers/rsc-parcel/src/browser.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import type { unstable_DecodeServerResponseFunction as DecodeServerResponseFunction } from "react-router";
import type { unstable_ServerPayload as ServerPayload } from "react-router";
import {
unstable_createCallServer as createCallServer,
unstable_getServerStream as getServerStream,
@@ -15,19 +15,16 @@ import {
// @ts-expect-error - no types for this yet
} from "react-server-dom-parcel/client";

const decode: DecodeServerResponseFunction = (body) =>
createFromReadableStream(body);

// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
createCallServer({
decode,
encodeAction: (args) => encodeReply(args),
createFromReadableStream,
encodeReply,
})
);

// Get and decode the initial server payload
decode(getServerStream()).then((payload) => {
createFromReadableStream(getServerStream()).then((payload: ServerPayload) => {
// @ts-expect-error - on 18 types, requires 19.
startTransition(async () => {
const formState =
@@ -36,7 +33,10 @@ decode(getServerStream()).then((payload) => {
hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter decode={decode} payload={payload} />
<RSCHydratedRouter
payload={payload}
createFromReadableStream={createFromReadableStream}
/>
</StrictMode>,
{
// @ts-expect-error - no types for this yet
8 changes: 4 additions & 4 deletions integration/helpers/rsc-parcel/src/prerender.tsx
Original file line number Diff line number Diff line change
@@ -9,16 +9,16 @@ import { createFromReadableStream } from "react-server-dom-parcel/client.edge";

export async function prerender(
request: Request,
callServer: (request: Request) => Promise<Response>,
fetchServer: (request: Request) => Promise<Response>,
bootstrapScriptContent: string | undefined
): Promise<Response> {
return await routeRSCServerRequest({
// The incoming request.
request,
// How to call the React Server.
callServer,
// How to fetch from the React Server.
fetchServer,
// Provide the React Server touchpoints.
decode: createFromReadableStream,
createFromReadableStream,
// Render the router to HTML.
async renderHTML(getPayload) {
const payload = await getPayload();
25 changes: 5 additions & 20 deletions integration/helpers/rsc-parcel/src/server.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { createRequestListener } from "@mjackson/node-fetch-server";
import express from "express";
import type {
unstable_DecodeCallServerFunction as DecodeCallServerFunction,
unstable_DecodeFormActionFunction as DecodeFormActionFunction,
} from "react-router";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
import {
decodeAction,
@@ -19,24 +15,13 @@ import { prerender } from "./prerender" with { env: "react-client" };
import { routes } from "./routes";
import { assets } from "./parcel-entry-wrapper"

// Decode and load actions by ID to support post-hydration server actions.
const decodeCallServer: DecodeCallServerFunction = async (actionId, reply) => {
const args = await decodeReply(reply);
const action = await loadServerAction(actionId);
return action.bind(null, ...args);
};

// Decode and load actions by form data to pre-hydration server actions.
const decodeFormAction: DecodeFormActionFunction = async (formData) => {
return await decodeAction(formData);
};

function callServer(request: Request) {
function fetchServer(request: Request) {
return matchRSCServerRequest({
// Provide the React Server touchpoints.
decodeCallServer,
decodeFormAction,
decodeReply,
decodeAction,
decodeFormState,
loadServerAction,
// The incoming request.
request,
// The app routes.
@@ -74,7 +59,7 @@ app.use(
createRequestListener((request) =>
prerender(
request,
callServer,
fetchServer,
(assets as unknown as { bootstrapScript?: string }).bootstrapScript
)
)
14 changes: 6 additions & 8 deletions integration/helpers/rsc-vite/src/entry.browser.tsx
Original file line number Diff line number Diff line change
@@ -5,22 +5,17 @@ import {
encodeReply,
setServerCallback,
} from "@hiogawa/vite-rsc/browser";
import type { unstable_DecodeServerResponseFunction as DecodeServerResponseFunction } from "react-router";
import {
unstable_createCallServer as createCallServer,
unstable_getServerStream as getServerStream,
unstable_RSCHydratedRouter as RSCHydratedRouter,
} from "react-router";
import type { unstable_ServerPayload as ServerPayload } from "react-router";

const decode: DecodeServerResponseFunction = (
body: ReadableStream<Uint8Array>
) => createFromReadableStream(body);

setServerCallback(
createCallServer({
decode,
encodeAction: (args) => encodeReply(args),
createFromReadableStream,
encodeReply,
})
);

@@ -29,7 +24,10 @@ createFromReadableStream<ServerPayload>(getServerStream()).then((payload) => {
hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter decode={decode} payload={payload as any} />
<RSCHydratedRouter
payload={payload}
createFromReadableStream={createFromReadableStream}
/>
</StrictMode>
);
});
Loading