Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion docs/start/rsc/route-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function MyRouteComponent() {
}
```

You can also export it as `Component` if that's your think.
You can also export it as `Component` if that's your thing.

### Props passed to the Component

Expand Down
2 changes: 1 addition & 1 deletion integration/helpers/rsc-parcel-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.24",
"parcel-config-react-router-experimental": "1.0.25",
"typescript": "^5.1.6"
},
"dependencies": {
Expand Down
2 changes: 2 additions & 0 deletions integration/helpers/rsc-parcel/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "react-router";
import {
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
// @ts-expect-error - no types for this yet
Expand All @@ -19,6 +20,7 @@ import {
setServerCallback(
createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
})
);
Expand Down
6 changes: 4 additions & 2 deletions integration/helpers/rsc-parcel/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRequestListener } from "@mjackson/node-fetch-server";
import express from "express";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
import {
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
Expand All @@ -18,6 +19,7 @@ import { assets } from "./parcel-entry-wrapper"
function fetchServer(request: Request) {
return matchRSCServerRequest({
// Provide the React Server touchpoints.
createTemporaryReferenceSet,
decodeReply,
decodeAction,
decodeFormState,
Expand All @@ -27,8 +29,8 @@ function fetchServer(request: Request) {
// The app routes.
routes,
// Encode the match with the React Server implementation.
generateResponse(match) {
return new Response(renderToReadableStream(match.payload), {
generateResponse(match, options) {
return new Response(renderToReadableStream(match.payload, options), {
status: match.statusCode,
headers: match.headers,
});
Expand Down
2 changes: 2 additions & 0 deletions integration/helpers/rsc-vite/src/entry.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import {
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
} from "@hiogawa/vite-rsc/browser";
Expand All @@ -15,6 +16,7 @@ import type { unstable_RSCPayload as RSCPayload } from "react-router";
setServerCallback(
createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
})
);
Expand Down
6 changes: 4 additions & 2 deletions integration/helpers/rsc-vite/src/entry.rsc.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
createTemporaryReferenceSet,
decodeAction,
decodeReply,
loadServerAction,
Expand All @@ -10,13 +11,14 @@ import { routes } from "./routes";

export async function fetchServer(request: Request) {
return await matchRSCServerRequest({
createTemporaryReferenceSet,
decodeReply,
decodeAction,
loadServerAction,
request,
routes,
generateResponse(match) {
return new Response(renderToReadableStream(match.payload), {
generateResponse(match, options) {
return new Response(renderToReadableStream(match.payload, options), {
status: match.statusCode,
headers: match.headers,
});
Expand Down
68 changes: 68 additions & 0 deletions integration/rsc/rsc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,74 @@ implementations.forEach((implementation) => {
// Ensure this is using RSC
validateRSCHtml(await page.content());
});

test("Supports React Server Function References", async ({ page }) => {
let port = await getPort();
stop = await setupRscTest({
implementation,
port,
files: {
"src/routes/home.actions.ts": js`
"use server";

export async function incrementCounter({count, ref}: {count: number; ref: unknown}, formData: FormData) {
return {count: count + parseInt(formData.get("by") as string || "1", 10), ref};
}
`,
"src/routes/home.tsx": js`
export { default } from "./home.client";
`,
"src/routes/home.client.tsx": js`
"use client";

import { useActionState } from "react";

import { incrementCounter } from "./home.actions";

const ogRef = {};
export default function HomeRoute() {
const [{count,ref}, incrementCounterAction, incrementing] = useActionState(incrementCounter, {count: 0, ref: ogRef});

return (
<div>
<h2 data-home>Home: ({count})</h2>
<h2 data-home-ref>{ref === ogRef ? "good" : "bad"}</h2>
<form action={incrementCounterAction}>
<button type="submit" data-submit>
{incrementing ? "Updating via Server Function" : "Update via Server Function"}
</button>
</form>
</div>
);
}
`,
},
});

await page.goto(`http://localhost:${port}/`);

// Verify initial server render
await page.waitForSelector("[data-home]");
expect(await page.locator("[data-home]").textContent()).toBe(
"Home: (0)"
);
await expect(page.locator("[data-home-ref]")).toHaveText("good");

// Submit the form to trigger server function
await page.click("[data-submit]");

// Verify server function updated the UI
await expect(page.locator("[data-home]")).toHaveText("Home: (1)");
await expect(page.locator("[data-home-ref]")).toHaveText("good");

// Submit again to ensure server functions work repeatedly
await page.click("[data-submit]");
await expect(page.locator("[data-home]")).toHaveText("Home: (2)");
await expect(page.locator("[data-home-ref]")).toHaveText("good");

// Ensure this is using RSC
validateRSCHtml(await page.content());
});
});

test.describe("Errors", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/react-router/index-react-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
export { matchRSCServerRequest as unstable_matchRSCServerRequest } from "./lib/rsc/server.rsc";

export type {
CreateFromReadableStreamFunction as unstable_CreateFromReadableStreamFunction,
DecodeActionFunction as unstable_DecodeActionFunction,
DecodeFormStateFunction as unstable_DecodeFormStateFunction,
DecodeReplyFunction as unstable_DecodeReplyFunction,
Expand Down
7 changes: 5 additions & 2 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,15 @@ export type { Register } from "./lib/types/register";
export { href } from "./lib/href";

// RSC
export type { EncodeReplyFunction as unstable_EncodeReplyFunction } from "./lib/rsc/browser";
export type {
BrowserCreateFromReadableStreamFunction as unstable_BrowserCreateFromReadableStreamFunction,
EncodeReplyFunction as unstable_EncodeReplyFunction,
} from "./lib/rsc/browser";
export {
createCallServer as unstable_createCallServer,
RSCHydratedRouter as unstable_RSCHydratedRouter,
} from "./lib/rsc/browser";
export type { SSRCreateFromReadableStreamFunction as unstable_SSRCreateFromReadableStreamFunction } from "./lib/rsc/server.ssr";
export {
routeRSCServerRequest as unstable_routeRSCServerRequest,
RSCStaticRouter as unstable_RSCStaticRouter,
Expand All @@ -300,7 +304,6 @@ import type { matchRSCServerRequest } from "./lib/rsc/server.rsc";
export declare const unstable_matchRSCServerRequest: typeof matchRSCServerRequest;

export type {
CreateFromReadableStreamFunction as unstable_CreateFromReadableStreamFunction,
DecodeActionFunction as unstable_DecodeActionFunction,
DecodeFormStateFunction as unstable_DecodeFormStateFunction,
DecodeReplyFunction as unstable_DecodeReplyFunction,
Expand Down
46 changes: 32 additions & 14 deletions packages/react-router/lib/rsc/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import type {
RSCPayload,
RSCRouteManifest,
RSCRenderPayload,
CreateFromReadableStreamFunction,
} from "./server.rsc";
import type {
DataStrategyFunction,
Expand All @@ -41,7 +40,19 @@ import {
} from "../dom/ssr/routes";
import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries";

export type EncodeReplyFunction = (args: unknown[]) => Promise<BodyInit>;
export type BrowserCreateFromReadableStreamFunction = (
body: ReadableStream<Uint8Array>,
{
temporaryReferences,
}: {
temporaryReferences: unknown;
}
) => Promise<unknown>;

export type EncodeReplyFunction = (
args: unknown[],
options: { temporaryReferences: unknown }
) => Promise<BodyInit>;

declare global {
interface Window {
Expand All @@ -53,10 +64,12 @@ declare global {

export function createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
fetch: fetchImplementation = fetch,
}: {
createFromReadableStream: CreateFromReadableStreamFunction;
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
createTemporaryReferenceSet: () => unknown;
encodeReply: EncodeReplyFunction;
fetch?: (request: Request) => Promise<Response>;
}) {
Expand All @@ -65,9 +78,10 @@ export function createCallServer({
let actionId = (window.__routerActionID =
(window.__routerActionID ??= 0) + 1);

const temporaryReferences = createTemporaryReferenceSet();
const response = await fetchImplementation(
new Request(location.href, {
body: await encodeReply(args),
body: await encodeReply(args, { temporaryReferences }),
method: "POST",
headers: {
Accept: "text/x-component",
Expand All @@ -78,9 +92,9 @@ export function createCallServer({
if (!response.body) {
throw new Error("No response body");
}
const payload = (await createFromReadableStream(
response.body
)) as RSCPayload;
const payload = (await createFromReadableStream(response.body, {
temporaryReferences,
})) as RSCPayload;

if (payload.type === "redirect") {
if (payload.reload) {
Expand Down Expand Up @@ -159,7 +173,7 @@ function createRouterFromPayload({
payload,
}: {
payload: RSCPayload;
createFromReadableStream: CreateFromReadableStreamFunction;
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
fetchImplementation: (request: Request) => Promise<Response>;
}) {
if (window.__router) return window.__router;
Expand Down Expand Up @@ -261,7 +275,7 @@ export function getRSCSingleFetchDataStrategy(
getRouter: () => DataRouter,
ssr: boolean,
basename: string | undefined,
createFromReadableStream: CreateFromReadableStreamFunction,
createFromReadableStream: BrowserCreateFromReadableStreamFunction,
fetchImplementation: (request: Request) => Promise<Response>
): DataStrategyFunction {
// TODO: Clean this up with a shared type
Expand Down Expand Up @@ -345,7 +359,7 @@ export function getRSCSingleFetchDataStrategy(
}

function getFetchAndDecodeViaRSC(
createFromReadableStream: CreateFromReadableStreamFunction,
createFromReadableStream: BrowserCreateFromReadableStreamFunction,
fetchImplementation: (request: Request) => Promise<Response>
): FetchAndDecodeFunction {
return async (
Expand Down Expand Up @@ -375,7 +389,9 @@ function getFetchAndDecodeViaRSC(
invariant(res.body, "No response body to decode");

try {
const payload = (await createFromReadableStream(res.body)) as RSCPayload;
const payload = (await createFromReadableStream(res.body, {
temporaryReferences: undefined,
})) as RSCPayload;
if (payload.type === "redirect") {
return {
status: res.status,
Expand Down Expand Up @@ -434,7 +450,7 @@ export function RSCHydratedRouter({
payload,
routeDiscovery = "eager",
}: {
createFromReadableStream: CreateFromReadableStreamFunction;
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
fetch?: (request: Request) => Promise<Response>;
payload: RSCPayload;
routeDiscovery?: "eager" | "lazy";
Expand Down Expand Up @@ -733,7 +749,7 @@ function getManifestUrl(paths: string[]): URL | null {

async function fetchAndApplyManifestPatches(
paths: string[],
createFromReadableStream: CreateFromReadableStreamFunction,
createFromReadableStream: BrowserCreateFromReadableStreamFunction,
fetchImplementation: (request: Request) => Promise<Response>,
signal?: AbortSignal
) {
Expand All @@ -755,7 +771,9 @@ async function fetchAndApplyManifestPatches(
throw new Error("Unable to fetch new route matches from the server");
}

let payload = (await createFromReadableStream(response.body)) as RSCPayload;
let payload = (await createFromReadableStream(response.body, {
temporaryReferences: undefined,
})) as RSCPayload;
if (payload.type !== "manifest") {
throw new Error("Failed to patch routes");
}
Expand Down
Loading