diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx index 0f3c353ba3d..f0a30a3a1b1 100644 --- a/packages/remix-react/__tests__/components-test.tsx +++ b/packages/remix-react/__tests__/components-test.tsx @@ -293,7 +293,7 @@ describe("", () => { describe("", () => { it("handles empty default export objects from the compiler", () => { window.__remixContext = { - ssrMatches: ["root", "empty"], + url: "/", state: { loaderData: {}, }, diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index f0394dbe2ac..c0014b1c1e1 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -25,7 +25,7 @@ import { initFogOfWar, useFogOFWarDiscovery } from "./fog-of-war"; /* eslint-disable prefer-let/prefer-let */ declare global { var __remixContext: { - ssrMatches: string[]; + url: string; basename?: string; state: HydrationState; criticalCss?: string; @@ -194,6 +194,29 @@ if (import.meta && import.meta.hot) { */ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { if (!router) { + // Hard reload if the path we tried to load is not the current path. + // This is usually the result of 2 rapid back/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. Avoid comparing search params because of CDNs which + // can be configured to ignore certain params and only pathname is relevant + // towards determining the route matches. + let initialPathname = window.__remixContext.url; + let hydratedPathname = window.location.pathname; + if ( + initialPathname !== hydratedPathname && + !window.__remixContext.isSpaMode + ) { + let errorMsg = + `Initial URL (${initialPathname}) does not match URL at time of hydration ` + + `(${hydratedPathname}), reloading page...`; + console.error(errorMsg); + window.location.reload(); + // Get out of here so the reload can happen - don't create the router + // since it'll then kick off unnecessary route.lazy() loads + return <>; + } + // When single fetch is enabled, we need to suspend until the initial state // snapshot is decoded into window.__remixContext.state if (window.__remixContext.future.unstable_singleFetch) { @@ -247,35 +270,6 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { window.location, window.__remixContext.basename ); - - // Hard reload if the matches we rendered on the server aren't the matches - // we matched in the client, otherwise we'll try to hydrate without the - // right modules and throw a hydration error, which can put React into an - // infinite hydration loop when hydrating the full `` document. - // This is usually the result of 2 rapid back/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 ssrMatches = window.__remixContext.ssrMatches; - let hasDifferentSSRMatches = - (initialMatches || []).length !== ssrMatches.length || - !(initialMatches || []).every((m, i) => ssrMatches[i] === m.route.id); - - if (hasDifferentSSRMatches && !window.__remixContext.isSpaMode) { - let ssr = ssrMatches.join(","); - let client = (initialMatches || []).map((m) => m.route.id).join(","); - let errorMsg = - `SSR Matches (${ssr}) do not match client matches (${client}) at ` + - `time of hydration , reloading page...`; - console.error(errorMsg); - - window.location.reload(); - - // Get out of here so the reload can happen - don't create the router - // since it'll then kick off unnecessary route.lazy() loads - return <>; - } - if (initialMatches) { for (let match of initialMatches) { let routeId = match.route.id; diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 865bae76063..35c2ebfef58 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -481,7 +481,7 @@ async function handleDocumentRequest( staticHandlerContext: context, criticalCss, serverHandoffString: createServerHandoffString({ - ssrMatches: context.matches.map((m) => m.route.id), + url: context.location.pathname, basename: build.basename, criticalCss, future: build.future, @@ -558,7 +558,7 @@ async function handleDocumentRequest( ...entryContext, staticHandlerContext: context, serverHandoffString: createServerHandoffString({ - ssrMatches: context.matches.map((m) => m.route.id), + url: context.location.pathname, basename: build.basename, future: build.future, isSpaMode: build.isSpaMode, diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts index 124cb5e0711..7328388ac3d 100644 --- a/packages/remix-server-runtime/serverHandoff.ts +++ b/packages/remix-server-runtime/serverHandoff.ts @@ -20,7 +20,7 @@ export function createServerHandoffString(serverHandoff: { // we'd end up including duplicate info state?: ValidateShape; criticalCss?: string; - ssrMatches: string[]; + url: string; basename: string | undefined; future: FutureConfig; isSpaMode: boolean;