Skip to content

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

Merged
matej21 merged 3 commits into
mainfrom
feat/use-api-static-collection
Aug 24, 2026
Merged

feat(bindx-react): createComponent().use() + crash-proof static selection analysis#62
matej21 merged 3 commits 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.

Rebased onto main (#85)

componentFactory.ts conflicted. main, via #85, subscribes implicit entity props
by having entityPropNames populated before useRenderProps runs, which needs
ensureImplicitCollected() to be called from ComponentImpl — the call this
branch had removed under design point 2.

Resolved by keeping the render-side call and layering this branch on top of it:
the tri-state guard, the tolerant scalar stand-ins, the try/catch that degrades
to a partial selection, and the .use() loop. Both fixes survive — implicit props
stay subscribed, and a render body that touches a scalar prop no longer takes the
whole selection down (#57). What is given up is design point 2's optimization that
a component nobody statically consumes is never analyzed at all; analysis can
still run from render, it just cannot crash any more.

Also dropped a stale @typescript-eslint/ban-types disable — that rule is not in
the current config and the lint gate now reports directives it cannot resolve.

Post-merge gate: bun run typecheck clean, bun run lint 0 errors / 17 warnings,
bun run test 1,969 passed across 201 files.

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 a production admin app via source sync: both collectionSafeT workarounds and the PageForm wrapper removed, full admin suite 288/288 green.

🤖 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
matej21 and others added 2 commits August 24, 2026 17:26
Conflict in `componentFactory.ts`: main (via #85) subscribes implicit
entity props by populating `entityPropNames` before `useRenderProps`,
which requires `ensureImplicitCollected()` to run from `ComponentImpl`;
this branch had removed that call so render bodies never trigger
analysis.

Resolved by keeping the render-side call and layering this branch's work
on top — the tri-state guard, the tolerant scalar stand-ins, the
try/catch that degrades to a partial selection, and the `.use()` loop.
Both fixes survive: implicit props stay subscribed, and a render body
that touches a scalar prop no longer takes the whole selection down.
What is given up is this branch's optimization that a component nobody
statically consumes is never analyzed at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EmqgPtZvAfpvWKymfFZCEX
`@typescript-eslint/ban-types` is not in the current config, and the lint
gate now reports directives for rules it cannot find.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EmqgPtZvAfpvWKymfFZCEX
@matej21
matej21 merged commit 88676e3 into main Aug 24, 2026
10 of 12 checks passed
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