remix: [Bug]: App crash on fast browser navigation

What version of Remix are you using?

1.1.3

What version of Node are you using? Minimum supported version is 14.

16.0.0

Steps to Reproduce

  1. Create a new Remix app with npx create-remix@latest.
  2. Choose the Remix dev server.
  3. Install dependencies using yarn install.
  4. Next to app/routes/index.tsx, add app/routes/other.tsx with a component that simply renders <>Other</>
  5. In app/routes/index.tsx, add <Link to="/other">Other</Link> somewhere, with import { Link } from "remix";.
  6. Run your app using yarn dev or yarn start; the error happens for both dev and prod builds.
  7. Open your browser and create a new browser tab (the bug happens with both Firefox and Chrome).
  8. Go to your app’s URL.
  9. Click the link to Other.
  10. Click the browser back button two times. The tab should now show your browser’s default UI for new tabs.
  11. Now very quickly click the browser’s forward button twice.
  12. The app should now crash. If it doesn’t you didn’t click the forward button fast enough.

Expected Behavior

The page content should be displayed without any errors.

Actual Behavior

In the simple repo above, the app crashes and there is the following stack trace in the dev console. In my real app in a production build, this issue then causes #1678, basically making the entire browser tab unresponsive.

components.js:470 Uncaught TypeError: Cannot read properties of undefined (reading 'meta')
    at Meta (components.js:470:21)
    at renderWithHooks (react-dom.development.js:14985:18)
    at mountIndeterminateComponent (react-dom.development.js:17811:13)
    at beginWork (react-dom.development.js:19049:16)
    at HTMLUnknownElement.callCallback2 (react-dom.development.js:3945:14)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:16)
    at invokeGuardedCallback (react-dom.development.js:4056:31)
    at beginWork$1 (react-dom.development.js:23964:7)
    at performUnitOfWork (react-dom.development.js:22776:12)
    at workLoopSync (react-dom.development.js:22707:5)
Meta @ components.js:470
renderWithHooks @ react-dom.development.js:14985
mountIndeterminateComponent @ react-dom.development.js:17811
beginWork @ react-dom.development.js:19049
callCallback2 @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
beginWork$1 @ react-dom.development.js:23964
performUnitOfWork @ react-dom.development.js:22776
workLoopSync @ react-dom.development.js:22707
renderRootSync @ react-dom.development.js:22670
performSyncWorkOnRoot @ react-dom.development.js:22293
scheduleUpdateOnFiber @ react-dom.development.js:21881
updateContainer @ react-dom.development.js:25482
(anonymous) @ react-dom.development.js:26021
unbatchedUpdates @ react-dom.development.js:22431
legacyRenderSubtreeIntoContainer @ react-dom.development.js:26020
hydrate2 @ react-dom.development.js:26086
(anonymous) @ entry.client.tsx:4```

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 13
  • Comments: 23 (13 by maintainers)

Commits related to this issue

Most upvoted comments

I think I’m gonna call this the goldilocks bug. It’s not just an issue of clicking too fast, or too slow. It seems to happen if you click forward, then wait just the right amount of time for the modules to start loading for index.tsx and then click the forward button a second time during the index chunk loads.

I could never reproduce it normally, but once I turned on network throttling and waited long enough to see the .js files start loading I could reproduce it.

I believe this is what’s happening:

  • Forward click - Remix renders / (index route) on the server
    • Remix sends back the proper modules for entry-client and the index route
    • Network requests go out for entry-client and index
  • While they are loading, second forward click
    • popstate is ignored since we haven’t finished loading the chunks yet and therefore haven’t instantiated our client side router and it’s popstate listener
    • Once the chunks load and go to hydrate, it attempts to hydrate /other since that’s the current URL

A few off-the-cuff options:

  1. Easier fix - track the URL at page load and if it’s not the same at hydration start (once JS chunks have loaded) we force a hard reload of /other
  2. Harder fix - same detection, but instead of a hard reload we use the manifest to kick off loads for the /other chunks and delay hydration until they complete. That introduces the case for the same issue on a third click, so that would have to do the check each time the new “initial” route chunks load.

I prefer 1 😃

A similar error occurred when I stupidly rendered <Scripts/> twice in root.tsx. Maybe this is helpful for someone.

import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export default function App() {
  return (
    <html>
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
        <Scripts /> {/* Duplicate! */}
      </body>
    </html>
  );
}

components:js:

Uncaught TypeError: Cannot destructure property 'default' of 'routeModules[id]' as it is undefined.
    at RemixRoute (components.js:179:14)
    at renderWithHooks (react-dom.development.js:14985:18)
    at updateFunctionComponent (react-dom.development.js:17356:20)
    at beginWork (react-dom.development.js:19063:16)
    at HTMLUnknownElement.callCallback2 (react-dom.development.js:3945:14)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:16)
    at invokeGuardedCallback (react-dom.development.js:4056:31)
    at beginWork$1 (react-dom.development.js:23964:7)
    at performUnitOfWork (react-dom.development.js:22776:12)
    at workLoopSync (react-dom.development.js:22707:5)

The error occurs only on <Link/> navigation. On refresh the route loads fine.

I’m running into this as well.

A trivial repro is:

  1. clone the remix repository and create the default playground app.
  2. cd to playground/<app> start it
  3. open localhost:3000, in chrome set your network speed to fast 3g
  4. click the “signup” link
  5. click back twice, so you’re on the default chrome page (no url loaded)
  6. click browser forward button once, and before entry.client.tsx loads, click forward again
  7. url should now be localhost:3000/join
  8. 💥 app crashes

hypothesis:

  1. routeModules is loaded from the the root route
  2. when entry.client.tsx finishes loading it tries to load the route module for /join which is not part of the routeModules map and it blows up

hypothetical fix:

  1. before running any client code, ensure the route module for the given route is loaded

Running into the same issue caused by fast navigation between pages

This seems to be caused by a race condition in the Remix code.

As far as I understand, the loadRouteModule function in routeModules.ts loads the JavaScript file related to a route and then adds an entry to the routeModules cache.

The Meta component uses this cache to do its thing.

If you’re doing the browser navigation slowly, everything works as expected: loadRouteModule loads the JS file, updates the cache and only then does Meta try to use it. If you’re fast, however, loadRouteModule hasn’t updated the routeModules cache yet, but Meta tries to access it anyway, resulting in the observed crash.

I’m not sure how to fix this, the code that is involved is quite… complicated and I’ve not yet been able to completely understand it.