Skip to content

fix(router): abandon a navigation whose navigator went away - #763

Merged
natew merged 1 commit into
mainfrom
fix/linkto-orphaned-navigation
Aug 22, 2026
Merged

fix(router): abandon a navigation whose navigator went away#763
natew merged 1 commit into
mainfrom
fix/linkto-orphaned-navigation

Conversation

@natew

@natew natew commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

linkTo awaits route preload and validation, and that work can outlive the tree that started the navigation. An auth gate swapping Slot for Redirect, or any host that hot-swaps an app by unmounting the React root and re-evaluating its bundle in the same realm, leaves the closure holding a container ref that is detached for good. Every method on a detached ref logs The 'navigation' object hasn't been initialized yet.

Two guards, both bailing rather than waiting, because the ref never re-attaches for an orphaned navigation:

  • after the awaits, before the ref is re-entered: bail and clear the loading state linkTo set before the preload, so nothing is left hanging.
  • inside the 16ms post-dispatch poll, which fires later than 16ms on a busy main thread and can outlive its tree.

dismiss()/dismissAll() are deliberately untouched. Those are synchronous user actions where a not-ready ref is genuine misuse and the log is informative.

The clearTimeout(interval) -> clearInterval(interval) change is consistency with the early return's teardown, not a fix: measured, clearTimeout already stopped the interval in both Chromium and node, which share one handle space.

linkToOrphaned.test.ts covers both paths deterministically, detaching the ref synchronously after replace() so it lands before linkTo resumes past its first await. Negative control: removing only the two guards and leaving clearInterval in place reproduces the production string in both tests.

Validation: full package suite 67 files / 575 tests, bun run typecheck, bun run lint 0 warnings 0 errors, all against the exact bytes on the branch.

linkTo checks assertIsReady once, then awaits the route preload, route
validation, and any module promise loadRoute throws. Every one of those can
outlive the tree that started the navigation: an auth gate that swaps Slot for
Redirect unmounts the subtree mid-flight, and a host that hot-swaps an app by
unmounting the React root and re-evaluating its bundle in the same realm leaves
the closure holding a container ref that is detached for good.

After the await, linkTo re-entered that ref unguarded (getRootState,
getCurrentRoute, resetRoot, dispatch), and its post-dispatch poll called
getCurrentRoute on a 16ms timer that outlives the tree outright. Every method on
a detached ref logs "The 'navigation' object hasn't been initialized yet", so an
orphaned navigation printed a react-navigation error into a console it no longer
had anything to do with.

Both sites now stop on !navigationRef.isReady(): there is no tree left to
dispatch into and the ref never re-attaches for this navigation, so waiting or
retrying would just spin. The post-await bail clears the loading state it set
before the preload.

linkToOrphaned.test.ts covers both: detaching the ref while linkTo is parked on
its preload, and detaching it after a completed dispatch so the poll fires
against nothing. Reverting either guard fails the matching test with the literal
production string.

Team-Machine-Session: m8354
@railway-app

railway-app Bot commented Aug 22, 2026

Copy link
Copy Markdown

🚅 Deployed to the one-pr-763 environment in onestack.dev

Service Status Web Updated (UTC)
one 🕗 Deploying (View Logs) Web Aug 22, 2026 at 2:31 am

@railway-app
railway-app Bot temporarily deployed to onestack.dev / one-pr-763 August 22, 2026 02:29 Destroyed
@natew
natew merged commit b9a2219 into main Aug 22, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant