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

fix: reload page when routeModules doesn't contain module for current route #6409

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
5 changes: 5 additions & 0 deletions .changeset/dual-forward-bug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

Detect mismatches between the initially loaded URL and the URL at the time we hydrate and trigger a hard reload if they do not match. This is an edge-case that can happen when the network is slowish and the user clicks forward into a Remix app and then clicks forward again while the initial JS chunks are loading.
84 changes: 84 additions & 0 deletions integration/browser-entry-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { test, expect } from "@playwright/test";

import type { AppFixture, Fixture } from "./helpers/create-fixture";
import { createFixture, js, createAppFixture } from "./helpers/create-fixture";
import { PlaywrightFixture } from "./helpers/playwright-fixture";

let fixture: Fixture;
let appFixture: AppFixture;

test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/routes/index.jsx": js`
import { Link } from "@remix-run/react";

export default function Index() {
return (
<div>
<div id="pizza">pizza</div>
<Link to="/burgers">burger link</Link>
</div>
)
}
`,

"app/routes/burgers.jsx": js`
export default function Index() {
return <div id="cheeseburger">cheeseburger</div>;
}
`,
},
});

// This creates an interactive app using puppeteer.
appFixture = await createAppFixture(fixture);
});

test.afterAll(() => appFixture.close());

test(
"expect to be able to browse backward out of a remix app, then forward " +
"twice in history and have pages render correctly",
async ({ page, browserName }) => {
test.skip(
browserName === "firefox",
"FireFox doesn't support browsing to an empty page (aka about:blank)"
);

let app = new PlaywrightFixture(appFixture, page);

// Slow down the entry chunk on the second load so the bug surfaces
let isSecondLoad = false;
await page.route(/entry/, async (route) => {
if (isSecondLoad) {
await new Promise((r) => setTimeout(r, 1000));
}
route.continue();
});

// This sets up the Remix modules cache in memory, priming the error case.
await app.goto("/");
await app.clickLink("/burgers");
expect(await page.content()).toContain("cheeseburger");
await page.goBack();
await page.waitForSelector("#pizza");
expect(await app.getHtml()).toContain("pizza");

// Takes the browser out of the Remix app
await page.goBack();
expect(page.url()).toContain("about:blank");

// Forward to / and immediately again to /burgers. This will trigger the
// error since we'll load __routeModules for / but then try to hydrate /burgers
isSecondLoad = true;
await page.goForward();
await page.goForward();
await page.waitForSelector("#cheeseburger");

// If we resolve the error, we should hard reload and eventually
// successfully render /burgers
await page.waitForSelector("#cheeseburger");
expect(await app.getHtml()).toContain("cheeseburger");
}
);
16 changes: 16 additions & 0 deletions packages/remix-react/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
/* eslint-disable prefer-let/prefer-let */
declare global {
var __remixContext: {
url: string;
state: HydrationState;
future: FutureConfig;
// The number of active deferred keys rendered on the server
Expand Down Expand Up @@ -177,6 +178,21 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement {
window.__remixContext.future.v2_normalizeFormMethod,
},
});

// Hard reload if the URL we tried to load is not the current URL.
// This is usually the result of 2 rapid backwards/forward clicks from an
// external site into a Remix app, where we initially start the load for
// one URL and while the JS chunks are loading a second forward click moves
// us to a new URL
let initialUrl = window.__remixContext.url;
let hydratedUrl = window.location.pathname + window.location.search;
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved
if (initialUrl !== hydratedUrl) {
let errorMsg =
`Initial URL (${initialUrl}) does not match URL at time of hydration ` +
`(${hydratedUrl}), reloading page...`;
console.error(errorMsg);
window.location.reload();
}
}

let [location, setLocation] = React.useState(router.state.location);
Expand Down
2 changes: 2 additions & 0 deletions packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ async function handleDocumentRequestRR(
routeModules: createEntryRouteModules(build.routes),
staticHandlerContext: context,
serverHandoffString: createServerHandoffString({
url: context.location.pathname + context.location.search,
state: {
loaderData: context.loaderData,
actionData: context.actionData,
Expand Down Expand Up @@ -307,6 +308,7 @@ async function handleDocumentRequestRR(
...entryContext,
staticHandlerContext: context,
serverHandoffString: createServerHandoffString({
url: context.location.pathname + context.location.search,
state: {
loaderData: context.loaderData,
actionData: context.actionData,
Expand Down
1 change: 1 addition & 0 deletions packages/remix-server-runtime/serverHandoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function createServerHandoffString<T>(serverHandoff: {
// Don't allow StaticHandlerContext to be passed in verbatim, since then
// we'd end up including duplicate info
state: ValidateShape<T, HydrationState>;
url: string;
future: FutureConfig;
dev?: { websocketPort: number };
}): string {
Expand Down