Fix children not rendering when render() called from different module

Create stable component function references at definition time instead of
on every render. This fixes Hono's JSX reconciliation losing track of
children when the component type identity keeps changing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Wanstrath 2026-01-20 21:02:58 -08:00
parent e772e0e711
commit 9b8d789da0

View File

@ -147,8 +147,10 @@ function makeStyle(def: TagDef) {
return style return style
} }
const ROOT_PROPS_KEY = '_forgeRootProps'
// turns a TagDef into a JSX component // turns a TagDef into a JSX component
function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<string, any>, partName?: string) { function makeComponent(baseName: string, rootDef: TagDef, partName?: string) {
const def = partName ? rootDef.parts?.[partName]! : rootDef const def = partName ? rootDef.parts?.[partName]! : rootDef
const base = def.base ?? 'div' const base = def.base ?? 'div'
@ -167,7 +169,7 @@ function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<stri
} }
} }
return ({ children, ...props }: { children: any, [key: string]: any }) => { return ({ [ROOT_PROPS_KEY]: rootProps = {}, children, ...props }: { [ROOT_PROPS_KEY]?: Record<string, any>, children: any, [key: string]: any }) => {
const classNames = [makeClassName(baseName, partName)] const classNames = [makeClassName(baseName, partName)]
const allProps = { ...rootProps, ...props } const allProps = { ...rootProps, ...props }
@ -286,14 +288,28 @@ export function define(nameOrDef: string | TagDef, defIfNamed?: TagDef) {
if (styles[name]) throw `${name} is already defined! Must use unique names.` if (styles[name]) throw `${name} is already defined! Must use unique names.`
registerStyles(name, def) registerStyles(name, def)
// ensure component function identity doesn't change between renders
const components: Record<string, (props: any) => any> = {}
for (const [partName] of Object.entries(def.parts ?? {}))
components[partName] = makeComponent(name, def, partName)
const RootComponent = makeComponent(name, def)
return (props: Record<string, any>) => { return (props: Record<string, any>) => {
const parts: Record<string, Function> = {} if (def.render) {
// For custom render, create parts object that injects _forgeRootProps
const parts: Record<string, (partProps: any) => any> = {}
for (const [part] of Object.entries(def.parts ?? {})) for (const [partName, Comp] of Object.entries(components))
parts[part] = makeComponent(name, def, props, part) parts[partName] = (partProps: any) => <Comp {...{ [ROOT_PROPS_KEY]: props }} {...partProps} />
parts.Root = makeComponent(name, def, props) parts.Root = (partProps: any) => <RootComponent {...{ [ROOT_PROPS_KEY]: props }} {...partProps} />
return def.render?.({ props, parts }) ?? <parts.Root {...props}>{props.children}</parts.Root> return def.render({ props, parts })
}
// Default render
return <RootComponent {...{ [ROOT_PROPS_KEY]: props }} {...props}>{props.children}</RootComponent>
} }
} }