Skip to content

lazyRouteComponent renders the error component after it has already triggered the stale-chunk reload #8377

Description

@abouroubi

Which project does this relate to?

Router

Describe the bug

When a code-split route chunk 404s after a deploy, lazyRouteComponent sets the tanstack_router_reload:<message> sessionStorage key, calls window.location.reload() and suspends with a never-resolving promise. That part works.

The problem is what happens between the reload() call and the navigation actually committing. reload() is asynchronous, and the error variable in the closure is never cleared. If Lazy re-renders during that gap (in our case the router store settling after the navigation is enough), the guard key is already set, so the if (!sessionStorage.getItem(storageKey)) check fails and the component falls through to throw error. The route's errorComponent then renders the Failed to fetch dynamically imported module TypeError for a moment, until the reload lands and the page comes back healthy.

This is point 2 in the description of #3262 ("I'm not 100% sure a re-render is possible here"). It is: our users report the error screen appearing and then the page refreshing on its own, and the error our errorComponent receives is that TypeError, which can only reach it through that branch.

The Solid and Vue lazyRouteComponent implementations have the same structure.

Steps to Reproduce the Bug or Issue

  1. Build an app with automatic code splitting and a defaultErrorComponent.
  2. Open it in a tab and stay on a route without refreshing.
  3. Deploy a new build so the chunk hashes change and the old chunks are no longer served.
  4. In the same tab, navigate to a route whose chunk has not been loaded yet.

Expected behavior

The pending state stays up until the reload completes.

Actual behavior

The errorComponent flashes with the import error, then the page reloads and works.

Proposed fix

Remember in the closure that a reload has been requested, and keep suspending on later renders instead of re-evaluating the sessionStorage guard:

let reloadRequested = false

const lazyComp = function Lazy(props: any) {
  if (error) {
    if (reloadRequested) {
      throw new Promise(() => {})
    }
    if (
      isModuleNotFoundError(error) &&
      !(isServer ?? typeof window === 'undefined') &&
      typeof sessionStorage !== 'undefined'
    ) {
      const storageKey = `tanstack_router_reload:${error.message}`
      if (!sessionStorage.getItem(storageKey)) {
        sessionStorage.setItem(storageKey, '1')
        reloadRequested = true
        window.location.reload()
        throw new Promise(() => {})
      }
    }
    throw error
  }
  // ...
}

The sessionStorage guard still prevents a reload loop across page loads; the closure flag only covers renders within the page that is already reloading.

Platform

  • Router / Start Version: 1.170.35 (same code on main)
  • Bundler: Vite
  • Browser: Chrome

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions