Skip to content

createComponent render crashes implicit collection when body uses a scalar prop #57

Description

@MalaRuze

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions