From c1af8d2709677b7e9721c1f0d8ac79718a529f4e Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Wed, 13 May 2026 20:46:44 -0400 Subject: [PATCH 1/2] refactor(KeepAlive): scope wrapChildren props, drop `as any` in getExisting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small cleanups to KeepAlive.tsx: 1. Stop leaking KeepAlive-level config onto the Lightning view `wrapChildren` spread the full props bag onto the inner `` and then overrode the handful it actually wanted (`onRemove`, `onRender`, `transition`, `preserve`, `forwardFocus`). That meant the view was also receiving `id`, `shouldDispose`, and the *original* (unchained) `onRemove` / `onRender` / `transition` before they were re-set — harmless for `id`, but `shouldDispose` is not a view-level concern and noisily lands on the Lightning element. Switch to passing only the props the view needs, and insert `props.children` explicitly. 2. Reflect that KeepAliveElement is built up lazily `getExisting` constructs a partial entry holding only `id` + the `isAlive` signal until the route mounts; it then `as any`-casts to satisfy a type that declared `owner`, `children`, and `dispose` as required. Mark those three fields optional on `KeepAliveElement` (matching reality), drop the `as any`, and tighten the casts at call-sites: `as unknown as ElementNode | undefined` so optional chaining works without lying about the value's presence. No behavior change. Co-Authored-By: Claude Opus 4.7 --- src/primitives/KeepAlive.tsx | 54 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/primitives/KeepAlive.tsx b/src/primitives/KeepAlive.tsx index 7cda551..62bdbc8 100644 --- a/src/primitives/KeepAlive.tsx +++ b/src/primitives/KeepAlive.tsx @@ -5,12 +5,15 @@ import { chainFunctions } from './utils/chainFunctions.js'; export interface KeepAliveElement { id: string; - owner: s.Owner | null; - children: s.JSX.Element; + // owner / children / dispose are populated lazily — the preload path + // creates a partial entry holding only `id` + `isAlive` until the route + // mounts, so callers must null-check before use. + owner?: s.Owner | null; + children?: s.JSX.Element; routeSignal?: s.Signal; isAlive?: s.Accessor; setIsAlive?: (v: boolean) => void; - dispose: () => void; + dispose?: () => void; } const keepAliveElements = new Map(); @@ -40,7 +43,7 @@ const _removeKeepAlive = ( ): void => { const element = map.get(id); if (element) { - (element.children as unknown as ElementNode)?.destroy(); + (element.children as unknown as ElementNode | undefined)?.destroy(); element.dispose?.(); map.delete(id); } @@ -53,7 +56,7 @@ export const removeKeepAliveRoute = (id: string): void => const _clearKeepAlive = (map: Map): void => { map.forEach((element) => { - (element.children as unknown as ElementNode)?.destroy(); + (element.children as unknown as ElementNode | undefined)?.destroy(); element.dispose?.(); }); map.clear(); @@ -91,15 +94,20 @@ function wrapChildren( ); const transition = props.transition || { alpha: true }; + // Only the props the view actually needs. Avoid `{...props}` so that + // KeepAlive-level config (`id`, `shouldDispose`, the unchained + // `onRemove`/`onRender`/`transition`) doesn't leak onto the Lightning + // element. return ( + > + {props.children} + ); } @@ -109,13 +117,13 @@ const createKeepAliveComponent = ( ) => { return (props: s.ParentProps) => { let existing = map.get(props.id); + const existingChild = existing?.children as ElementNode | undefined; if ( existing && - (props.shouldDispose?.(props.id) || - (existing.children as unknown as ElementNode)?.destroyed) + (props.shouldDispose?.(props.id) || existingChild?.destroyed) ) { - (existing.children as unknown as ElementNode).destroy(); + existingChild?.destroy(); existing.dispose?.(); map.delete(props.id); existing = undefined; @@ -138,8 +146,8 @@ const createKeepAliveComponent = ( }); return children; }); - } else if (existing && !existing.children) { - existing.children = s.runWithOwner(existing.owner, () => + } else if (!existing.children) { + existing.children = s.runWithOwner(existing.owner ?? null, () => wrapChildren(props, existing.setIsAlive), ); } @@ -175,18 +183,14 @@ export const KeepAliveRoute = ( const key = props.id || props.path; let savedFocusedElement: ElementNode | undefined; - const getExisting = () => { + const getExisting = (): KeepAliveElement => { let existing = keepAliveRouteElements.get(key); if (!existing) { const [isAlive, setIsAlive] = s.createSignal(true); - existing = { - id: key, - isAlive, - setIsAlive, - } as any; - keepAliveRouteElements.set(key, existing!); + existing = { id: key, isAlive, setIsAlive }; + keepAliveRouteElements.set(key, existing); } - return existing!; + return existing; }; const onRemove = chainFunctions(props.onRemove, (elm: ElementNode) => { @@ -216,13 +220,13 @@ export const KeepAliveRoute = ( const preload = props.preload ? (preloadProps: RoutePreloadFuncArgs) => { let existing = getExisting(); + const existingChild = existing.children as unknown as ElementNode | undefined; if ( - existing.children && - (props.shouldDispose?.(key) || - (existing.children as unknown as ElementNode)?.destroyed) + existingChild && + (props.shouldDispose?.(key) || existingChild.destroyed) ) { - (existing.children as unknown as ElementNode).destroy(); + existingChild.destroy(); existing.dispose?.(); keepAliveRouteElements.delete(key); existing = getExisting(); @@ -235,7 +239,7 @@ export const KeepAliveRoute = ( return props.preload!({ ...preloadProps, isAlive: existing.isAlive! }); }); } else if (existing.children) { - (existing.children as unknown as ElementNode)?.setFocus(); + (existing.children as unknown as ElementNode).setFocus(); return props.preload!({ ...preloadProps, isAlive: existing.isAlive! }); } else { return props.preload!({ From 7b30b0a4a06e8307de377520dea5b1f066a21255 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Wed, 13 May 2026 20:51:16 -0400 Subject: [PATCH 2/2] you do need to spread props --- src/primitives/KeepAlive.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/primitives/KeepAlive.tsx b/src/primitives/KeepAlive.tsx index 62bdbc8..9262138 100644 --- a/src/primitives/KeepAlive.tsx +++ b/src/primitives/KeepAlive.tsx @@ -94,20 +94,15 @@ function wrapChildren( ); const transition = props.transition || { alpha: true }; - // Only the props the view actually needs. Avoid `{...props}` so that - // KeepAlive-level config (`id`, `shouldDispose`, the unchained - // `onRemove`/`onRender`/`transition`) doesn't leak onto the Lightning - // element. return ( - {props.children} - + /> ); }