fix(router): prevent child routes from silently dropping when stored component is reattached into a recreated parent shell#69556
Open
arturovt wants to merge 1 commit into
Open
Conversation
…component is reattached into a recreated parent shell When a stored component (e.g. EventListCmp at path '') is reattached into a fresh parent shell (one that is destroyed and recreated on every navigation because it is not covered by the reuse strategy), child routes deeper than the stored component could fail to render. The root cause was in `ChildrenOutletContexts.onOutletDeactivated()`. It replaced `this.contexts` with a new empty Map and returned the old one for storage. When the shell was recreated, its fresh router-outlet created a brand-new OutletContext for the stored component, and `onOutletReAttached()` loaded the saved contexts into *that* new context's `.children`. However, the router-outlet *inside* the stored component (which stayed alive while detached) was injected with the *original* `ChildrenOutletContexts` at component creation time and held that reference for its lifetime. Because the original instance's Map had been replaced with an empty one, subsequent `activateWith()` calls on that outlet created grandchild components with a fresh, empty context. Those grandchildren's outlets registered themselves there, but the router traversed the *restored* context tree (a different object) and never found an outlet to activate into — so the grandchild component was silently dropped. Fix: keep the same Map instance in `onOutletDeactivated()` rather than swapping it out. This ensures that any outlet injected with the original `ChildrenOutletContexts` and the router's restored context tree both see the same backing Map after reattach. Stale outlet references left behind by destroyed components are already cleaned up naturally through `ngOnDestroy` → `onChildOutletDestroyed()`. Fixes angular#57285
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a stored component (e.g. EventListCmp at path '') is reattached into a fresh parent shell (one that is destroyed and recreated on every navigation because it is not covered by the reuse strategy), child routes deeper than the stored component could fail to render.
The root cause was in
ChildrenOutletContexts.onOutletDeactivated(). It replacedthis.contextswith a new empty Map and returned the old one for storage. When the shell was recreated, its fresh router-outlet created a brand-new OutletContext for the stored component, andonOutletReAttached()loaded the saved contexts into that new context's.children. However, the router-outlet inside the stored component (which stayed alive while detached) was injected with the originalChildrenOutletContextsat component creation time and held that reference for its lifetime. Because the original instance's Map had been replaced with an empty one, subsequentactivateWith()calls on that outlet created grandchild components with a fresh, empty context. Those grandchildren's outlets registered themselves there, but the router traversed the restored context tree (a different object) and never found an outlet to activate into — so the grandchild component was silently dropped.Fix: keep the same Map instance in
onOutletDeactivated()rather than swapping it out. This ensures that any outlet injected with the originalChildrenOutletContextsand the router's restored context tree both see the same backing Map after reattach. Stale outlet references left behind by destroyed components are already cleaned up naturally throughngOnDestroy→onChildOutletDestroyed().Fixes #57285