feat(bindx-react): createComponent().use() + crash-proof static selection analysis - #62
Merged
Merged
Conversation
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #57.
Problem
createComponent(...).render(...)crashed implicit-selection collection whenever the render body used a scalar (.props<>()) prop value — e.g. calling a translatort(key)or readinglabels.heading. The collection pass fedundefinedfor 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 coarsetry/catchinuseSelectionCollection, silently dropping the whole JSX-derived selection (including sibling components' fields) from the fetch plan — a real data-loss (noindexand friends never fetched, downstream shippedcollectionSafeTworkarounds).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:
ComponentImplno longer triggers it; it runs solely fromgetSelection,$propNamefragment 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 (collectingterminates self-recursive components).console.errorwith component attribution (displayName).analyzeJsxcatches per node — one broken component no longer costs its siblings their selection..use(fn)— runtime-only values, hooks allowed: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.tsconflicted. main, via #85, subscribes implicit entity propsby having
entityPropNamespopulated beforeuseRenderPropsruns, which needsensureImplicitCollected()to be called fromComponentImpl— the call thisbranch 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/catchthat degradesto a partial selection, and the
.use()loop. Both fixes survive — implicit propsstay 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-typesdisable — that rule is not inthe current config and the lint gate now reports directives it cannot resolve.
Post-merge gate:
bun run typecheckclean,bun run lint0 errors / 17 warnings,bun run test1,969 passed across 201 files.Verification
createComponentScalarPropCollection.test.tsx) now passes.createComponentUse.test.tsx: analysis skipsuse()while still collecting fields; hooks insideuse(); chaining; sibling isolation on analysis crash; partial capture before a throw.mainwith the playground running;bindx-formformRelations dirty-tracking fails onmaintoo).collectionSafeTworkarounds and thePageFormwrapper removed, full admin suite 288/288 green.🤖 Generated with Claude Code
https://claude.ai/code/session_016eijo87HdS4vA6D2ZcS6KL