Skip to content

React server dom vite #31768

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

Closed
wants to merge 13 commits into from
Closed
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
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ module.exports = {
'packages/react-server-dom-webpack/**/*.js',
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-vite/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
Expand Down Expand Up @@ -488,6 +489,9 @@ module.exports = {
parcelRequire: 'readonly',
},
},
{
files: ['packages/react-server-dom-vite/**/*.js'],
},
{
files: ['packages/scheduler/**/*.js'],
globals: {
Expand Down
1 change: 1 addition & 0 deletions ReactVersions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const stablePackages = {
'react-server-dom-webpack': ReactVersion,
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-server-dom-vite': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.32.0',
'react-refresh': '0.17.0',
Expand Down
7 changes: 7 additions & 0 deletions fixtures/flight-vite-mini/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
dist
node_modules
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
128 changes: 128 additions & 0 deletions fixtures/flight-vite-mini/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { type Page, expect, test } from "@playwright/test";
import { createEditor } from "./helper";

test("client reference", async ({ page }) => {
await page.goto("/");
await page.getByText("[hydrated: 1]").click();
await page.getByText("Client counter: 0").click();
await page
.getByTestId("client-counter")
.getByRole("button", { name: "+" })
.click();
await page.getByText("Client counter: 1").click();
await page.reload();
await page.getByText("Client counter: 0").click();
});

test("server reference in server @js", async ({ page }) => {
await testServerAction(page);
});

test.describe(() => {
test.use({ javaScriptEnabled: false });
test("server reference in server @nojs", async ({ page }) => {
await testServerAction(page);
});
});

async function testServerAction(page: Page) {
await page.goto("/");
await page.getByText("Server counter: 0").click();
await page
.getByTestId("server-counter")
.getByRole("button", { name: "+" })
.click();
await page.getByText("Server counter: 1").click();
await page.goto("/");
await page.getByText("Server counter: 1").click();
await page
.getByTestId("server-counter")
.getByRole("button", { name: "-" })
.click();
await page.getByText("Server counter: 0").click();
}

test("server reference in client @js", async ({ page }) => {
await testServerAction2(page, { js: true });
});

test.describe(() => {
test.use({ javaScriptEnabled: false });
test("server reference in client @nojs", async ({ page }) => {
await testServerAction2(page, { js: false });
});
});

async function testServerAction2(page: Page, options: { js: boolean }) {
await page.goto("/");
if (options.js) {
await page.getByText("[hydrated: 1]").click();
}
await page.locator('input[name="x"]').fill("2");
await page.locator('input[name="y"]').fill("3");
await page.locator('input[name="y"]').press("Enter");
await expect(page.getByTestId("calculator-answer")).toContainText("5");
await page.locator('input[name="x"]').fill("2");
await page.locator('input[name="y"]').fill("three");
await page.locator('input[name="y"]').press("Enter");
await expect(page.getByTestId("calculator-answer")).toContainText(
"(invalid input)",
);
if (options.js) {
await expect(page.locator('input[name="x"]')).toHaveValue("2");
await expect(page.locator('input[name="y"]')).toHaveValue("three");
} else {
await expect(page.locator('input[name="x"]')).toHaveValue("");
await expect(page.locator('input[name="y"]')).toHaveValue("");
}
}

test("client hmr @dev", async ({ page }) => {
await page.goto("/");
await page.getByText("[hydrated: 1]").click();
// client +1
await page.getByText("Client counter: 0").click();
await page
.getByTestId("client-counter")
.getByRole("button", { name: "+" })
.click();
await page.getByText("Client counter: 1").click();
// edit client
using file = createEditor("src/app/client.tsx");
file.edit((s) => s.replace("Client counter", "Client [EDIT] counter"));
await page.getByText("Client [EDIT] counter: 1").click();
});

test("server hmr @dev", async ({ page }) => {
await page.goto("/");
await page.getByText("[hydrated: 1]").click();

// server +1
await page.getByText("Server counter: 0").click();
await page
.getByTestId("server-counter")
.getByRole("button", { name: "+" })
.click();
await page.getByText("Server counter: 1").click();

// client +1
await page.getByText("Client counter: 0").click();
await page
.getByTestId("client-counter")
.getByRole("button", { name: "+" })
.click();
await page.getByText("Client counter: 1").click();

// edit server
using file = createEditor("src/app/index.tsx");
file.edit((s) => s.replace("Server counter", "Server [EDIT] counter"));
await page.getByText("Server [EDIT] counter: 1").click();
await page.getByText("Client counter: 1").click();

// server -1
await page
.getByTestId("server-counter")
.getByRole("button", { name: "-" })
.click();
await page.getByText("Server [EDIT] counter: 0").click();
});
15 changes: 15 additions & 0 deletions fixtures/flight-vite-mini/e2e/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from "node:fs";

export function createEditor(filepath: string) {
let init = fs.readFileSync(filepath, "utf-8");
let data = init;
return {
edit(editFn: (data: string) => string) {
data = editFn(data);
fs.writeFileSync(filepath, data);
},
[Symbol.dispose]() {
fs.writeFileSync(filepath, init);
},
};
}
27 changes: 27 additions & 0 deletions fixtures/flight-vite-mini/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"private": true,
"type": "module",
"scripts": {
"build-deps": "rm -rf node_modules/{react,react-dom/react-server-dom-vite} && cd ../.. && RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-jsx-runtime.react-server,react-jsx-dev-runtime.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-vite/ --type=NODE_DEV,NODE_PROD && cp -r ./build/node_modules/* ./fixtures/flight-vite-mini/node_modules/",
"dev": "vite dev",
"build": "vite build --app",
"preview": "vite preview",
"format": "npx biome format --write",
"test-e2e": "playwright test",
"test-e2e-preview": "E2E_PREVIEW=1 playwright test"
},
"dependencies": {
"react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@playwright/test": "^1.49.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.3"
}
}
30 changes: 30 additions & 0 deletions fixtures/flight-vite-mini/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig, devices } from "@playwright/test";

const port = Number(process.env.E2E_PORT || 6174);
const isPreview = Boolean(process.env.E2E_PREVIEW);
const command = isPreview
? `yarn preview --port ${port} --strict-port`
: `yarn dev --port ${port} --strict-port`;

export default defineConfig({
testDir: "e2e",
use: {
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
],
webServer: {
command,
port,
},
grepInvert: isPreview ? /@dev/ : /@build/,
forbidOnly: !!process.env["CI"],
retries: process.env["CI"] ? 2 : 0,
reporter: "list",
});
Binary file added fixtures/flight-vite-mini/public/favicon.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions fixtures/flight-vite-mini/src/app/action-by-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use server";

export async function add(_prev: unknown, formData: FormData) {
let x = formData.get("x");
let y = formData.get("y");
if (typeof x === "string" && typeof y === "string") {
let x2 = parseFloat(x);
let y2 = parseFloat(y);
if (!Number.isNaN(x2) && !Number.isNaN(y2)) {
return x2 + y2;
}
}
return "(invalid input)";
}
12 changes: 12 additions & 0 deletions fixtures/flight-vite-mini/src/app/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use server";

let counter = 0;

export function getCounter() {
return counter;
}

export async function changeCounter(formData: FormData) {
const change = Number(formData.get("change"));
counter += change;
}
62 changes: 62 additions & 0 deletions fixtures/flight-vite-mini/src/app/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import React from "react";
import { add } from "./action-by-client";

export function Counter() {
const [count, setCount] = React.useState(0);
return (
<div data-testid="client-counter" style={{ padding: "0.5rem" }}>
<div>Client counter: {count}</div>
<div>
<button onClick={() => setCount((c) => c - 1)}>-</button>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
</div>
);
}

export function Hydrated() {
return <pre>[hydrated: {Number(useHydrated())}]</pre>;
}

function useHydrated() {
return React.useSyncExternalStore(
React.useCallback(() => () => {}, []),
() => true,
() => false,
);
}

export function Calculator() {
const [returnValue, formAction, _isPending] = React.useActionState(add, null);
const [x, setX] = React.useState("");
const [y, setY] = React.useState("");

return (
<form
action={formAction}
style={{ padding: "0.5rem" }}
data-testid="calculator"
>
<div>Calculator</div>
<div style={{ display: "flex", gap: "0.3rem" }}>
<input
name="x"
style={{ width: "2rem" }}
value={x}
onChange={(e) => setX(e.target.value)}
/>
+
<input
name="y"
style={{ width: "2rem" }}
value={y}
onChange={(e) => setY(e.target.value)}
/>
=<span data-testid="calculator-answer">{returnValue ?? "?"}</span>
</div>
<button hidden></button>
</form>
);
}
28 changes: 28 additions & 0 deletions fixtures/flight-vite-mini/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { changeCounter, getCounter } from "./action";
import { Calculator, Counter, Hydrated } from "./client";

export async function IndexPage() {
return (
<div>
<div>server random: {Math.random().toString(36).slice(2)}</div>
<Hydrated />
<Counter />
<form
action={changeCounter}
data-testid="server-counter"
style={{ padding: "0.5rem" }}
>
<div>Server counter: {getCounter()}</div>
<div>
<button name="change" value="-1">
-
</button>
<button name="change" value="+1">
+
</button>
</div>
</form>
<Calculator />
</div>
);
}
25 changes: 25 additions & 0 deletions fixtures/flight-vite-mini/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export function Layout(props: React.PropsWithChildren) {
return (
<html>
<head>
<meta charSet="UTF-8" />
<title>react-server</title>
<meta
name="viewport"
content="width=device-width, height=device-height, initial-scale=1.0"
/>
</head>
<body>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/other">Other</a>
</li>
</ul>
{props.children}
</body>
</html>
);
}
3 changes: 3 additions & 0 deletions fixtures/flight-vite-mini/src/app/other.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function OtherPage() {
return <div>Other Page</div>;
}
Loading