Skip to content

fix(lazyLoad): abort URL-sourced transition when lazy loaded states do not match the URL#851

Open
simshanith wants to merge 1 commit into
ui-router:masterfrom
simshanith:fix/lazyload-sync-abort
Open

fix(lazyLoad): abort URL-sourced transition when lazy loaded states do not match the URL#851
simshanith wants to merge 1 commit into
ui-router:masterfrom
simshanith:fix/lazyload-sync-abort

Conversation

@simshanith

@simshanith simshanith commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Fix for #850: when a URL matches a future state's prefix but nothing in the lazy-loaded module matches the full URL, the original URL-sourced transition resumed after urlService.sync() and activated the already-deregistered .** placeholder, superseding the otherwise() transition started by sync().

Live repro (plunkr, against published 6.1.2): https://plnkr.co/edit/gist:147d1aa6774f01b699b8cce35847e946?open=index.html&preview — open the preview pane and click "Deep link to #/contacts/no/such/page".

Fix

In retryTransition() (src/hooks/lazyLoad.ts), abort the original transition by returning false after delegating to sync(). The STATE-rule branch above already avoids sync() precisely because of this supersede hazard; this closes the same hazard in the fall-through branch.

Downstream impact

The changed line is only reachable when a URL-sourced transition enters a lazyLoad state and, after a successful load, no registered state matches the URL. Successful lazy loads take the STATE-redirect branch; programmatic ($state.go) retries take the non-URL branch — both unchanged.

In the affected (previously broken) case:

  • With an otherwise() rule (e.g. sample-app-angularjs): deep links now activate the otherwise rule instead of a blank, deregistered placeholder.
  • Without one: the transition aborts and the router stays on its current state, instead of activating a phantom state that stateRegistry.get() no longer knows about (which broke subsequent relative navigation / active-state checks).
  • No new console noise: ABORTED rejections return before defaultErrorHandler is invoked (stateService.ts), the transitionTo promise is already silenced, and urlRouter.update() on abort is gated on isLatest (false here), so the URL doesn't flap. Global onError hooks observe one ABORTED transition where they previously observed a spurious onSuccess.

Tests

Two regression tests in test/lazyLoadSpec.ts (both fail without the fix, verified):

  • deep-linking to a URL that prefix-matches a .** future state but matches nothing in the loaded module now activates the otherwise() rule's state
  • the original transition to the deregistered placeholder is rejected with RejectType.ABORTED

Full suite: 587/590 pass (3 pre-existing skips), same as master baseline.

Alternative considered

A stricter variant (simshanith#2) aborts only when the original target is no longer registered. In practice they behave identically: reaching the fall-through implies the placeholder's URL rule no longer matched, i.e. it was deregistered. The unconditional abort is simpler and lets whatever sync() decides win consistently.

Closes #850

…o not match the URL

When a URL matches a future state's prefix but nothing in the lazy
loaded module matches the full URL, retryTransition() delegates to
urlService.sync() but returned undefined, letting the original
transition resume and activate the deregistered .** placeholder,
superseding the otherwise() transition started by sync().

Return false after sync() to abort the original transition so the
sync()-initiated transition (e.g. the otherwise() rule) can complete.

Closes ui-router#850
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.

lazyLoad: URL-sourced transition to a replaced .** placeholder supersedes the otherwise() transition started by sync()

1 participant