Skip to content

Commit 525e424

Browse files
committed
PR feedback
1 parent b80ca86 commit 525e424

File tree

3 files changed

+51
-13
lines changed

3 files changed

+51
-13
lines changed

integration/spa-mode-test.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
99
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
1010
import { createProject, viteBuild } from "./helpers/vite.js";
1111

12+
// SSR'd useId value we can assert against pre- and post-hydration
13+
const USE_ID_VALUE = ":R1:";
14+
1215
test.describe("SPA Mode", () => {
1316
let fixture: Fixture;
1417
let appFixture: AppFixture;
@@ -26,9 +29,11 @@ test.describe("SPA Mode", () => {
2629
});
2730
`,
2831
"app/root.tsx": js`
32+
import * as React from "react";
2933
import { Form, Link, Links, Meta, Outlet, Scripts } from "@remix-run/react";
3034
3135
export default function Root() {
36+
let id = React.useId();
3237
return (
3338
<html lang="en">
3439
<head>
@@ -37,6 +42,7 @@ test.describe("SPA Mode", () => {
3742
</head>
3843
<body>
3944
<h1 data-root>Root</h1>
45+
<pre data-use-id>{id}</pre>
4046
<nav>
4147
<Link to="/about">/about</Link>
4248
<br/>
@@ -66,6 +72,10 @@ test.describe("SPA Mode", () => {
6672
}
6773
6874
export function HydrateFallback() {
75+
const id = React.useId();
76+
const [hydrated, setHydrated] = React.useState(false);
77+
React.useEffect(() => setHydrated(true), []);
78+
6979
return (
7080
<html lang="en">
7181
<head>
@@ -74,14 +84,16 @@ test.describe("SPA Mode", () => {
7484
</head>
7585
<body>
7686
<h1 data-loading>Loading SPA...</h1>
87+
<pre data-use-id>{id}</pre>
88+
{hydrated ? <h3 data-hydrated>Hydrated</h3> : null}
7789
<Scripts />
7890
</body>
7991
</html>
8092
);
8193
}
82-
`,
94+
`,
8395
"app/routes/_index.tsx": js`
84-
import { useState, useEffect } from "react";
96+
import * as React from "react";
8597
import { useLoaderData } from "@remix-run/react";
8698
8799
export function meta({ data }) {
@@ -90,14 +102,17 @@ test.describe("SPA Mode", () => {
90102
}];
91103
}
92104
93-
export function clientLoader() {
105+
export async function clientLoader({ request }) {
106+
if (new URL(request.url).searchParams.has('slow')) {
107+
await new Promise(r => setTimeout(r, 500));
108+
}
94109
return "Index Loader Data";
95110
}
96111
97112
export default function Component() {
98113
let data = useLoaderData();
99-
const [mounted, setMounted] = useState(false);
100-
useEffect(() => setMounted(true), []);
114+
const [mounted, setMounted] = React.useState(false);
115+
React.useEffect(() => setMounted(true), []);
101116
102117
return (
103118
<>
@@ -159,7 +174,7 @@ test.describe("SPA Mode", () => {
159174
let error = useRouteError();
160175
return <pre data-error>{error.data}</pre>
161176
}
162-
`,
177+
`,
163178
},
164179
});
165180

@@ -241,6 +256,9 @@ test.describe("SPA Mode", () => {
241256
expect(await page.locator("[data-loading]").textContent()).toBe(
242257
"Loading SPA..."
243258
);
259+
expect(await page.locator("[data-use-id]").textContent()).toBe(
260+
USE_ID_VALUE
261+
);
244262
expect(await page.locator("title").textContent()).toBe(
245263
"Index Title: undefined"
246264
);
@@ -263,6 +281,25 @@ test.describe("SPA Mode", () => {
263281
);
264282
});
265283

284+
test("hydrates a proper useId value", async ({ page }) => {
285+
let app = new PlaywrightFixture(appFixture, page);
286+
await app.goto("/?slow");
287+
288+
// We should hydrate the same useId value in HydrateFallback that we
289+
// rendered on the server above
290+
await page.waitForSelector("[data-hydrated]");
291+
expect(await page.locator("[data-use-id]").textContent()).toBe(
292+
USE_ID_VALUE
293+
);
294+
295+
// Once hydrated, we should get a different useId value from the root component
296+
await page.waitForSelector("[data-route]");
297+
expect(await page.locator("[data-route]").textContent()).toBe("Index");
298+
expect(await page.locator("[data-use-id]").textContent()).not.toBe(
299+
USE_ID_VALUE
300+
);
301+
});
302+
266303
test("navigates and calls loaders", async ({ page }) => {
267304
let app = new PlaywrightFixture(appFixture, page);
268305
await app.goto("/");

packages/remix-dev/vite/plugin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type * as Vite from "vite";
44
import { type BinaryLike, createHash } from "node:crypto";
55
import * as path from "node:path";
6+
import * as url from "node:url";
67
import * as fse from "fs-extra";
78
import babel from "@babel/core";
89
import {
@@ -419,7 +420,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
419420
...resolveServerBuildConfig(),
420421
};
421422

422-
// Log warning
423+
// Log warning for incompatible vite config flags
423424
if (isSpaMode && unstable_serverBundles) {
424425
console.warn(
425426
colors.yellow(
@@ -1517,7 +1518,7 @@ async function handleSpaMode(
15171518
// proper HydrateFallback ... or not! Maybe they have a static landing page
15181519
// generated from routes/_index.tsx.
15191520
let serverBuildPath = path.join(serverBuildDirectoryPath, serverBuildFile);
1520-
let build = await import(`file://${serverBuildPath}`);
1521+
let build = await import(url.pathToFileURL(serverBuildPath).toString());
15211522
let { createRequestHandler: createHandler } = await import("@remix-run/node");
15221523
let handler = createHandler(build, viteConfig.mode);
15231524
let response = await handler(new Request("http://localhost/"));

packages/remix-react/routes.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export function createServerRoutes(
7777
routesByParentId: Record<
7878
string,
7979
Omit<EntryRoute, "children">[]
80-
> = groupRoutesByParentId(manifest)
80+
> = groupRoutesByParentId(manifest),
81+
spaModeLazyPromise = Promise.resolve({ Component: () => null })
8182
): DataRouteObject[] {
8283
return (routesByParentId[parentId] || []).map((route) => {
8384
let routeModule = routeModules[route.id];
@@ -108,9 +109,7 @@ export function createServerRoutes(
108109
// implementation here though - just need a `lazy` prop to tell the RR
109110
// rendering where to stop
110111
lazy:
111-
isSpaMode && route.id !== "root"
112-
? () => Promise.resolve({ Component: () => null })
113-
: undefined,
112+
isSpaMode && route.id !== "root" ? () => spaModeLazyPromise : undefined,
114113
// For partial hydration rendering, we need to indicate when the route
115114
// has a loader/clientLoader, but it won't ever be called during the static
116115
// render, so just give it a no-op function so we can render down to the
@@ -126,7 +125,8 @@ export function createServerRoutes(
126125
future,
127126
isSpaMode,
128127
route.id,
129-
routesByParentId
128+
routesByParentId,
129+
spaModeLazyPromise
130130
);
131131
if (children.length > 0) dataRoute.children = children;
132132
return dataRoute;

0 commit comments

Comments
 (0)