deferred-jsx-props
A prop whose markup is built outside a render — pushed onto a queue, held in a signal, sent from a promise — is typed `() => JSX.Element`, never `JSX.Element`. Applies whenever adding a prop or an API
SKILL.md.claude/skills/deferred-jsx-props/SKILL.md
GitHubSolid's compiler wraps a component's dynamic props in memos when the element is constructed. Construct it inside a render and those memos get that render's owner and are disposed with it. Construct it in an event handler, a .then, or a setTimeout, and they have no owner, live for the life of the page, and Solid says so:
computations created outside a
createRootorrenderwill never be disposed
The rule
- Markup passed parent to child stays
JSX.Element. The compiler makes it a getter evaluated inside the parent's render, which has an owner.title,trigger,footer,asideacrosssrc/components/styledare all correct as they are. - Markup that is stored and drawn later is
() => JSX.Element. The holder calls it where it draws it.
// Bad — every caller builds this in a handler or a promise
export interface ToastRequest {
art?: JSX.Element;
}
toast.push({ art: <ItemSprite item={stack.item} size={24} label="" /> });
// Good — built under the card that draws it
export interface ToastRequest {
art?: () => JSX.Element;
}
toast.push({ art: () => <ItemSprite item={stack.item} size={24} label="" /> });
The holder renders it with an explicit call, and tests the value rather than the markup:
<Show when={props.toast.art != null}>
<div class="…">{props.toast.art?.()}</div>
</Show>
How to tell which you have
Ask where the JSX is written. If the answer is "inside another component's JSX", it is a plain prop. If it is "inside a function that runs after something happened", it is a thunk.