Add consume() and captureContext() to createContext#21488
Open
NullVoxPopuli-ai-agent wants to merge 2 commits into
Open
Add consume() and captureContext() to createContext#21488NullVoxPopuli-ai-agent wants to merge 2 commits into
NullVoxPopuli-ai-agent wants to merge 2 commits into
Conversation
`context.consume()` with no argument behaves exactly like reading `context.value`. Passed a handle from the new `captureContext()` export, it resolves the nearest `<Provide>` above the captured render-tree position instead -- which works outside of rendering, where the ambient reads throw. This is the way to read a context from event handlers, timers, and other async callbacks that run after the render stack has unwound. A capture is an opaque frozen handle validated through a WeakMap in the render-scope layer (plus a type-only brand), so it can't be forged or introspected. One handle works with every context: it captures the render-tree position, not any one context's value, and resolution happens at consume time so the value read is the provider's current @value rather than a snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| * class ThemedButton extends Component { | ||
| * // Field initializers run in the constructor, during rendering, so this | ||
| * // captures the position of <ThemedButton/> in the render tree. | ||
| * context = captureContext(); |
Contributor
There was a problem hiding this comment.
we don't want this -- a user should not need to worry about this and it should be an implementation detail.
Contributor
Author
There was a problem hiding this comment.
Removed in 4fae99c — captureContext() is gone from the public API. The VM now associates the component instance with its render-scope node automatically when the component's self is read (VM_GET_COMPONENT_SELF_OP), so user code just passes the component itself: theme.consume(this). The association is a WeakMap in the render-scope layer; self refs are const refs, so the eager read consumes no tracking tags, and template-only components (NULL_REFERENCE self) are skipped since there's no instance to pass.
Review feedback: users should not need to capture a handle themselves.
Drop the public captureContext() export entirely. Instead, the VM
associates the user-facing component instance with its render-scope
node when the component's self becomes known (VM_GET_COMPONENT_SELF_OP,
via a WeakMap in the render-scope layer), so an event handler can just
pass the component itself:
onClick = () => theme.consume(this);
Component self refs are const refs wrapping the instance, so the eager
read consumes no tracking tags; template-only components have a
NULL_REFERENCE self and are skipped (there is no instance to pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Targets #21450's branch (
nvp/implement-context), adding the escape hatch for reading a context outside of rendering.The problem
The ambient reads —
context.value, and any code path that runs during rendering — resolve against the currently-rendering node. Event handlers, timers, and other async callbacks run after the render stack has unwound, so there is no ambient position to resolve from, andvalue(correctly) throws. But an event handler is exactly the kind of place you want the app's nearestThemeContext/SessionContext/etc.The API
One addition to the object returned by
createContext:context.consume(component?)— with no argument it is identical to readingvalue(same resolution, same harm-reduction errors). Passed a component instance, it resolves the nearest<Provide>above that component's position in the render tree — no ambient render state required.How the instance maps to a render-tree position is an implementation detail: the VM associates the user-facing component instance with its render-scope node when the component's
selfbecomes known (VM_GET_COMPONENT_SELF_OP), via a WeakMap in the render-scope layer. User code never captures anything by hand. (An earlier revision exposed an explicitcaptureContext()step; removed per review.)Semantics
consumetime. The value read is the provider's current@value, not a snapshot from render time (covered by a test that flips a@trackedvalue between reads).consume(component)with an object that never rendered throws a pointed error (the association WeakMap doesn't know it); a rendered component with no matching provider above it throws the same harm-reduction style error asvalue. The "outside of rendering" error for ambient reads now points atconsume(this)as the fix.NULL_REFERENCEself and are skipped — there is no instance for user code to pass.Implementation notes
This rides on the base branch's retained node tree:
RenderScopeTrackerkeeps parent links per node, so an associated node stays resolvable after the stack unwinds. The render-scope layer gainsassociateRenderScope()/lookupRenderContextFor()and a sharedlookupInChain()walk; the association lives exactly as long as the component instance (WeakMap). Worth noting for the design comparison: the #21486 frame-epoch variant deliberately does not retain per-node parent chains, so this API would need a different mechanism there.Testing
{{on "click"}}dispatch asserting the ambient read throws whileconsume(this)resolves; nearest-provider at different depths; one-instance-many-contexts; current-value-not-snapshot; missing-provider and never-rendered-object errors.consume()/consume(component); docs coverage.🤖 Generated with Claude Code