Summary
createComponent(...).render(...) crashes during implicit-selection collection if the render body uses a scalar (.props<>()) prop value — e.g. calling a translator t(key) or reading labels.x. Collection feeds undefined for every scalar prop and runs the render pass unguarded, so the call/dereference throws and takes down the whole component render.
Environment
Reproduction
const Comp = createComponent()
.entity('entity', schema.Article)
.props<{ t: (key: string) => string }>()
.render(({ entity, t }) => (
<div>
<h2>{t('some.heading')}</h2>
<Field field={entity.title} />
</div>
))
// Triggering implicit-selection collection (e.g. via the `$entity` fragment
// getter, or simply by rendering the component) throws:
Comp.$entity // TypeError: t is not a function
A scalar object prop fails the same way: ({ labels }) => <h2>{labels.heading}</h2> →
TypeError: undefined is not an object (evaluating 'labels.heading').
The crash only manifests when the scalar is used in the synchronous render body. If the same t(...) call sits inside a relation/field render-prop callback (e.g. inside <HasOne>{e => …}</HasOne>), it does not crash — collectImplicitSelections wraps the collectSelection(jsx) tree walk in try/catch, but not the initial renderFn(propsProxy) pass — so the inconsistency is easy to hit by accident.
Expected behavior
Using a scalar prop value in the render body is correct usage (no hook, no .value read on a ref) and should not crash collection. The collection pass should still discover entity fields (title above). Scalar props are not relevant to field selection, so they should either be passed through during collection (their real values are already available at the ComponentImpl(props) call site) or the render pass should degrade gracefully like the JSX tree walk already does.
Actual behavior
TypeError: t is not a function. (In 't("some.heading")', 't' is undefined)
The error propagates out of collection and crashes the React render (in dev, React reports a recovered concurrent-render error wrapping the TypeError).
Suspected root cause
packages/bindx-react/src/jsx/componentFactory.ts:
-
In collectImplicitSelections, the propsProxy get trap returns undefined for any prop that is neither an explicit nor implicit entity prop (the "scalar prop" branch):
// Scalar prop - return undefined
return undefined
-
The render pass that follows is unguarded, unlike the subsequent collectSelection(jsx) walk:
// Execute render to capture field accesses
const jsx = renderFn(propsProxy) // <-- not wrapped in try/catch
try {
collectSelection(jsx)
} catch {
// swallowed
}
So any synchronous use of a scalar prop value in the render body throws here and is never caught.
Note collectImplicitSelections is invoked from ensureImplicitCollected() inside ComponentImpl(props) — the real props (with the real scalar values) are in scope at that point but are not threaded into collection.
Suggested fix
Either (preferred) thread the real props into collectImplicitSelections and have the propsProxy return the real value for scalar (non-entity) props, so the render body sees the actual t / labels; or, as a minimal safety net, wrap the renderFn(propsProxy) pass in the same try/catch already used around collectSelection(jsx) (field accesses that happened before the throw are still captured in the scope tree). Deferring to maintainers on the preferred direction.
Workaround shipped downstream
We applied a temporary workaround in our project, marked
TODO [BindX] (<this-issue-url>): <description>. The workaround keeps all scalar-prop reads (the t(...) translator calls) out of the synchronous createComponent render body so the collection pass no longer crashes; we will remove it once this issue is resolved.
Summary
createComponent(...).render(...)crashes during implicit-selection collection if the render body uses a scalar (.props<>()) prop value — e.g. calling a translatort(key)or readinglabels.x. Collection feedsundefinedfor every scalar prop and runs the render pass unguarded, so the call/dereference throws and takes down the whole component render.Environment
@contember/bindx@0.1.43(version installed in the reporting project)contember/bindx@mainas of6f3c2f1tests/react/jsx/createComponentScalarPropCollection.test.tsxbug/scalar-prop-undefined-crashes-implicit-collectionReproduction
A scalar object prop fails the same way:
({ labels }) => <h2>{labels.heading}</h2>→TypeError: undefined is not an object (evaluating 'labels.heading').The crash only manifests when the scalar is used in the synchronous render body. If the same
t(...)call sits inside a relation/field render-prop callback (e.g. inside<HasOne>{e => …}</HasOne>), it does not crash —collectImplicitSelectionswraps thecollectSelection(jsx)tree walk in try/catch, but not the initialrenderFn(propsProxy)pass — so the inconsistency is easy to hit by accident.Expected behavior
Using a scalar prop value in the render body is correct usage (no hook, no
.valueread on a ref) and should not crash collection. The collection pass should still discover entity fields (titleabove). Scalar props are not relevant to field selection, so they should either be passed through during collection (their real values are already available at theComponentImpl(props)call site) or the render pass should degrade gracefully like the JSX tree walk already does.Actual behavior
The error propagates out of collection and crashes the React render (in dev, React reports a recovered concurrent-render error wrapping the TypeError).
Suspected root cause
packages/bindx-react/src/jsx/componentFactory.ts:In
collectImplicitSelections, thepropsProxygettrap returnsundefinedfor any prop that is neither an explicit nor implicit entity prop (the "scalar prop" branch):The render pass that follows is unguarded, unlike the subsequent
collectSelection(jsx)walk:So any synchronous use of a scalar prop value in the render body throws here and is never caught.
Note
collectImplicitSelectionsis invoked fromensureImplicitCollected()insideComponentImpl(props)— the realprops(with the real scalar values) are in scope at that point but are not threaded into collection.Suggested fix
Either (preferred) thread the real
propsintocollectImplicitSelectionsand have thepropsProxyreturn the real value for scalar (non-entity) props, so the render body sees the actualt/labels; or, as a minimal safety net, wrap therenderFn(propsProxy)pass in the same try/catch already used aroundcollectSelection(jsx)(field accesses that happened before the throw are still captured in the scope tree). Deferring to maintainers on the preferred direction.Workaround shipped downstream
We applied a temporary workaround in our project, marked
TODO [BindX] (<this-issue-url>): <description>. The workaround keeps all scalar-prop reads (thet(...)translator calls) out of the synchronouscreateComponentrender body so the collection pass no longer crashes; we will remove it once this issue is resolved.