Skip to content

feat(bindx-react): createComponent().use() + crash-proof static selection analysis#62

Open
matej21 wants to merge 1 commit into
mainfrom
feat/use-api-static-collection
Open

feat(bindx-react): createComponent().use() + crash-proof static selection analysis#62
matej21 wants to merge 1 commit into
mainfrom
feat/use-api-static-collection

Conversation

@matej21

@matej21 matej21 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Fixes #57.

Problem

createComponent(...).render(...) crashed implicit-selection collection whenever the render body used a scalar (.props<>()) prop value — e.g. calling a translator t(key) or reading labels.heading. The collection pass fed undefined for every scalar prop and ran the render unguarded, so the call/dereference threw.

Worse than the crash itself: when triggered through a fetching <Entity>'s walk, the throw was swallowed by a coarse try/catch in useSelectionCollection, silently dropping the whole JSX-derived selection (including sibling components' fields) from the fetch plan — a real data-loss (noindex and friends never fetched, downstream shipped collectionSafeT workarounds).

Design

Implicit collection is an abstract interpretation of the render body. This PR makes it total (never crashes out) and confines it to the static-analysis phase:

  1. Tolerant scalar stand-ins — during collection, scalar props resolve to an inert value that absorbs calls, property access, primitive coercion and iteration. Scalar content can never affect field selection, so the analysis must not need real values. (Known, accepted hole: indexing a real object with a mocked key — handled by 2 & 3.)
  2. Collection only from the static surfaceComponentImpl no longer triggers it; it runs solely from getSelection, $propName fragment getters, and the interfaces proxy. Render bodies are pure runtime code. Components whose selection nobody statically consumes are never collected (no dead work, no spurious dev errors). The guard is tri-state (collecting terminates self-recursive components).
  3. Graceful, loud degradation — scopes capture field accesses eagerly, so a mid-body throw now finalizes the partial selection instead of losing everything, and logs a console.error with component attribution (displayName). analyzeJsx catches per node — one broken component no longer costs its siblings their selection.
  4. .use(fn) — runtime-only values, hooks allowed:
const PageForm = createComponent()
	.entity('entity', schema.WebsitePageRevision)
	.use(() => ({ t: useT() }))
	.render(({ entity, t }) => (
		<InputField field={entity.page.title} label={t('wb.forms.page.titleLabel')} />
	))

Runs inside the React render (hooks, context all work) and merges into render props; chainable — later use() fns see earlier values. Static analysis skips it entirely; its outputs fall through to the scalar stand-ins. Type-wise the values exist only in render props, not on the public component props. This removes the thin wrapper components that existed only to thread hook-derived values in as props — wrappers that also broke selection discovery, since the JSX walk cannot see through plain components.

Verification

  • Repro test from the issue branch (createComponentScalarPropCollection.test.tsx) now passes.
  • New createComponentUse.test.tsx: analysis skips use() while still collecting fields; hooks inside use(); chaining; sibling isolation on analysis crash; partial capture before a throw.
  • Full suite: 1616 pass. Pre-existing failures unchanged (browser suite fails identically on main with the playground running; bindx-form formRelations dirty-tracking fails on main too).
  • Wired into the NPI admin via source sync: both collectionSafeT workarounds and the PageForm wrapper removed, full admin suite 288/288 green (companion PR in that repo).

🤖 Generated with Claude Code

https://claude.ai/code/session_016eijo87HdS4vA6D2ZcS6KL

…tion analysis

Fixes #57 — using a scalar prop value (a translator call, a labels object)
in a createComponent render body crashed implicit-selection collection with
"t is not a function", and one crashing component silently dropped its
siblings' selections from the fetch plan.

Implicit collection is an abstract interpretation of the render body; this
makes it total and confines it to the static-analysis phase:

- Scalar props get a tolerant stand-in during collection (callable,
  property-safe, primitive-coercible, iterable) instead of undefined, so
  render bodies using them survive and entity fields keep being captured.
- ComponentImpl no longer triggers collection — it runs only from the
  static surface (getSelection, $propName fragment getters, interfaces
  proxy). Render bodies are pure runtime code; components whose selection
  is never consumed statically are never collected.
- A throw during collection no longer costs the whole component (scopes
  capture eagerly; the partial selection is finalized on the throw path)
  and is reported loudly with component attribution instead of being
  swallowed. analyzeJsx catches per node, so a broken component no longer
  drops sibling selections.
- New .use(fn) builder step: runtime-only values with hooks allowed
  (useT(), useContext(), ...). Runs inside the React render and merges
  into render props; chainable, later fns see earlier values. Static
  analysis skips it entirely — its outputs are covered by the scalar
  stand-ins. This removes the need for thin wrapper components that only
  thread hook-derived values in as props (and which also broke selection
  discovery, because the JSX walk cannot see through plain components).

Repro test from the issue branch + .use() test suite included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016eijo87HdS4vA6D2ZcS6KL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createComponent render crashes implicit collection when body uses a scalar prop

1 participant