Skip to content
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

Minor SPA mode updates #8464

Merged
merged 1 commit into from
Jan 9, 2024
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
7 changes: 7 additions & 0 deletions .changeset/lazy-oranges-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@remix-run/dev": patch
"@remix-run/react": patch
---

- [REMOVE] Error if no `<Scripts>` included in root `<HydrateFallback>`
- [REMOVE] Don't render SSR scroll restoration script in SPA mode
97 changes: 97 additions & 0 deletions integration/spa-mode-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,103 @@ test.describe("SPA Mode", () => {
"See https://remix.run/future/spa-mode for more information."
);
});

test("errors on a non-200 status from entry.server.tsx", async () => {
let cwd = await createProject({
"vite.config.ts": js`
import { defineConfig } from "vite";
import { unstable_vitePlugin as remix } from "@remix-run/dev";

export default defineConfig({
plugins: [remix({ unstable_ssr: false })],
});
`,
"app/entry.server.tsx": js`
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
) {
const html = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
return new Response(html, {
headers: { "Content-Type": "text/html" },
status: 500,
});
}
`,
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";

export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}

export function HydrateFallback() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<h1>Loading...</h1>
<Scripts />
</body>
</html>
);
}
`,
});
let result = viteBuild({ cwd });
let stderr = result.stderr.toString("utf8");
expect(stderr).toMatch(
"SPA Mode: Received a 500 status code from `entry.server.tsx` while " +
"generating the `index.html` file."
);
expect(stderr).toMatch("<h1>Loading...</h1>");
});

test("errors if you do not include <Scripts> in your root <HydrateFallback>", async () => {
let cwd = await createProject({
"vite.config.ts": js`
import { defineConfig } from "vite";
import { unstable_vitePlugin as remix } from "@remix-run/dev";

export default defineConfig({
plugins: [remix({ unstable_ssr: false })],
});
`,
"app/root.tsx": String.raw`
export function HydrateFallback() {
return <h1>Loading</h1>
}
`,
});
let result = viteBuild({ cwd });
let stderr = result.stderr.toString("utf8");
expect(stderr).toMatch(
"SPA Mode: Did you forget to include <Scripts/> in your `root.tsx` " +
"`HydrateFallback` component? Your `index.html` file cannot hydrate " +
"into a SPA without `<Scripts />`."
);
});
});

test.describe("javascript disabled", () => {
Expand Down
22 changes: 19 additions & 3 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,11 +1490,27 @@ async function handleSpaMode(
let { createRequestHandler: createHandler } = await import("@remix-run/node");
let handler = createHandler(build, viteConfig.mode);
let response = await handler(new Request("http://localhost/"));
invariant(response.status === 200, "Error generating the index.html file");
let html = await response.text();
if (response.status !== 200) {
throw new Error(
`SPA Mode: Received a ${response.status} status code from ` +
`\`entry.server.tsx\` while generating the \`index.html\` file.\n${html}`
);
}

if (
!html.includes("window.__remixContext =") ||
!html.includes("window.__remixRouteModules =")
) {
throw new Error(
"SPA Mode: Did you forget to include <Scripts/> in your `root.tsx` " +
"`HydrateFallback` component? Your `index.html` file cannot hydrate " +
"into a SPA without `<Scripts />`."
);
}
Comment on lines +1501 to +1510
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detect if they forgot <Scripts> since SPA mode is useless without it


// Write out the index.html file for the SPA
let htmlPath = path.join(assetsBuildDirectory, "index.html");
await fse.writeFile(htmlPath, await response.text());
await fse.writeFile(path.join(assetsBuildDirectory, "index.html"), html);

viteConfig.logger.info(
"SPA Mode: index.html has been written to your " +
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const RemixContext = React.createContext<RemixContextObject | undefined>(
);
RemixContext.displayName = "Remix";

function useRemixContext(): RemixContextObject {
export function useRemixContext(): RemixContextObject {
let context = React.useContext(RemixContext);
invariant(context, "You must render this element inside a <Remix> element");
return context;
Expand Down
8 changes: 8 additions & 0 deletions packages/remix-react/scroll-restoration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "react-router-dom";

import type { ScriptProps } from "./components";
import { useRemixContext } from "./components";

let STORAGE_KEY = "positions";

Expand All @@ -22,6 +23,7 @@ export function ScrollRestoration({
}: ScriptProps & {
getKey?: ScrollRestorationPropsRR["getKey"];
}) {
let { isSpaMode } = useRemixContext();
let location = useLocation();
let matches = useMatches();

Expand All @@ -47,6 +49,12 @@ export function ScrollRestoration({
[]
);

// In SPA Mode, there's nothing to restore on initial render since we didn't
// render anything on the server
if (isSpaMode) {
return null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to do initial SSR scroll restoration in SPA mode since no content was SSR'd

}

let restoreScroll = ((STORAGE_KEY: string, restoreKey: string) => {
if (!window.history.state || !window.history.state.key) {
let key = Math.random().toString(32).slice(2);
Expand Down
1 change: 1 addition & 0 deletions templates/spa/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function HydrateFallback() {
<body>
<p>Loading...</p>
<Scripts />
<LiveReload />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed for HMR to wire up correctly

</body>
</html>
);
Expand Down