Skip to content

fix(KeepAliveRoute): preserve children getter to avoid creating sibling routes#9

Merged
chiefcll merged 1 commit into
mainfrom
fix/keepalive-route-eager-children-getter
May 14, 2026
Merged

fix(KeepAliveRoute): preserve children getter to avoid creating sibling routes#9
chiefcll merged 1 commit into
mainfrom
fix/keepalive-route-eager-children-getter

Conversation

@chiefcll
Copy link
Copy Markdown
Contributor

Summary

Two related fixes inside KeepAliveRoute:

  • Stale Show subscribed to a sibling outlet. The component wrapper used a spread to inject isAlive into the route props ({ ...childProps, isAlive }). That spread invokes the router's get children() getter, which builds a <Show when={routeStates()[i+1]} keyed> and subscribes it to routeStates. Because KeepAlive preserves the subtree across navigations, this Show stays alive after the user navigates away. When a later navigation populates routeStates()[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 (a KeepAliveRoute), then navigate to /examples/tmdb. TMDB is 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. The children getter stays on the prototype chain and is only invoked when the user component actually reads it. No Proxy, 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/router createRoutes). If KeepAliveRoute is re-evaluated, the new routeDef changes the key, which forces routeStates to dispose + recreate sibling contexts and re-invoke their components. Cache the resolved <Route> JSX per key so the routeDef identity is stable. The cache captures props at first invocation, so a clearKeepAliveRouteCache() escape hatch is exported for callers that need to refresh.

Test plan

  • Verified the repro in the demo app: starting at /browse/all then navigating to /examples/tmdb constructs TMDB exactly once after the fix (previously twice).
  • Confirm no regression in the normal mount/unmount/back-navigation cycle for KeepAliveRoute (focus restore, isAlive signal, shouldDispose, preload).
  • Manual test: re-entering the keep-alive route preserves state.
  • Manual test: forward navigation between two different KeepAliveRoutes.

🤖 Generated with Claude Code

…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>
@chiefcll chiefcll merged commit 959668b into main May 14, 2026
1 check 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