fix(KeepAliveRoute): preserve children getter to avoid creating sibling routes#9
Merged
Merged
Conversation
…ng routes
Two related fixes for KeepAliveRoute:
1. Stale Show subscribed to a sibling route's outlet
The component wrapper spread the route's `childProps` when invoking the
user component: `props.component({ ...childProps, isAlive })`. That
spread invokes the router's `get children()` getter, which constructs a
`<Show when={routeStates()[i+1]} keyed>` and subscribes it to
`routeStates`. KeepAlive preserves this subtree across navigations, so
the Show stays alive after the user navigates away. When a later
navigation populates `routeStates()[i+1]` (the keep-alive route's
would-be child index) with the matched context of an unrelated sibling
route, that stale Show fires and constructs the sibling's component out
of the preserved subtree.
Concrete repro: visit `/browse/:filter` (KeepAliveRoute), then navigate
to a sibling route whose nested component sits at the same depth (e.g.
`examples` -> `tmdb`). TMDB's body runs twice — once from the stale
Show in the preserved Browse subtree, once from the actual Portal
render after the lazy import resolves.
Fix: build the inner props object via `Object.create(childProps, ...)`
instead of spreading. The `children` getter stays on the prototype and
is only invoked when the user component actually reads it. No Proxy, so
older platforms (Tizen/WebOS) remain supported.
2. Route key drift on re-evaluation
`<Route>` returns a fresh routeDef object on every call, and solid-
router uses that object identity as the route key. If KeepAliveRoute
ever gets re-evaluated, the new routeDef would change the key, forcing
`routeStates` to dispose + recreate sibling contexts and re-invoke
their components. Cache the resolved `<Route>` JSX per key so the
routeDef identity is stable. Caveat documented inline: this captures
`props` at first invocation; export `clearKeepAliveRouteCache` for the
rare cases that need it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Summary
Two related fixes inside
KeepAliveRoute:Stale Show subscribed to a sibling outlet. The component wrapper used a spread to inject
isAliveinto the route props ({ ...childProps, isAlive }). That spread invokes the router'sget children()getter, which builds a<Show when={routeStates()[i+1]} keyed>and subscribes it torouteStates. Because KeepAlive preserves the subtree across navigations, this Show stays alive after the user navigates away. When a later navigation populatesrouteStates()[i+1]with an unrelated sibling route's context, the stale Show fires and constructs the sibling's component out of the preserved subtree.Concrete repro: start at
/browse/:filter(aKeepAliveRoute), then navigate to/examples/tmdb.TMDBis invoked twice — once from the stale Show in the preserved Browse subtree, once from the actual Portal render after the lazy import resolves.Fix: build the inner props via
Object.create(childProps, …)instead of spreading. Thechildrengetter stays on the prototype chain and is only invoked when the user component actually reads it. NoProxy, so older platforms (Tizen/WebOS) keep working.Route key drift on re-evaluation.
<Route>returns a fresh routeDef on every call, and solid-router uses that object identity as the route key (see@solidjs/routercreateRoutes). IfKeepAliveRouteis re-evaluated, the new routeDef changes the key, which forcesrouteStatesto dispose + recreate sibling contexts and re-invoke their components. Cache the resolved<Route>JSX per key so the routeDef identity is stable. The cache capturespropsat first invocation, so aclearKeepAliveRouteCache()escape hatch is exported for callers that need to refresh.Test plan
/browse/allthen navigating to/examples/tmdbconstructsTMDBexactly once after the fix (previously twice).KeepAliveRoute(focus restore,isAlivesignal,shouldDispose,preload).KeepAliveRoutes.🤖 Generated with Claude Code