Track the next step after plain non-computed member-chain binding:
- optional chaining, e.g.
x?.y.z,x.y?.z - computed access, e.g.
x[y].z,x.y[k]
This note is intentionally a follow-up to the plain member-chain work in 2026-04-05-rsc-member-chain-binding-plan.md. It is not part of the current cleanup / plain-chain implementation.
The current implementation is intentionally narrow:
- plain non-computed member chains like
x.y.zare captured precisely - unsupported hops stop capture at the last safe prefix
- examples:
x?.y.z-> bindxa.b?.c-> bind{ b: a.b }x[y].z-> bindx
This is a reasonable conservative failure mode, but it is not full support.
The current BindPath shape in src/transforms/hoist.ts
is effectively:
type BindPath = {
key: string
segments: string[]
}That is enough for x.y.z because codegen can reconstruct the bind expression
from the root identifier plus dot segments.
It is not enough for:
x?.y.zx.y?.zx[y].zx?.[y]
The missing information is not cosmetic. It changes semantics.
Each hop needs to preserve whether access is optional.
Example:
x?.y.zReconstructing this as x.y.z is wrong because the bind-time access becomes
stricter than the original expression.
Each computed hop needs the property expression, not just a string segment.
Example:
x[y].zThere is no way to reconstruct this faithfully from ["y", "z"], because the
first y is an expression, not a property name.
Computed access is not only a codegen problem. The key expression itself may close over outer variables, or it may be local to the action.
Outer-scope key:
function outer() {
let key = 'x'
let obj = {}
async function action() {
'use server'
return obj[key]
}
}Both obj and key are outer captures.
Action-local key:
function outer() {
let obj = {}
async function action() {
'use server'
let key = 'x'
return obj[key]
}
}Only obj is an outer capture; key is local to the action.
So any future obj[expr] support must treat the computed key as an ordinary
expression with its own scope resolution, not just as a printable suffix on a
member path.
To support these cases, BindPath needs richer per-hop metadata.
Sketch:
type BindSegment =
| { kind: 'property'; name: string; optional: boolean }
| { kind: 'computed'; expr: Node; optional: boolean }
type BindPath = {
key: string
segments: BindSegment[]
}This is enough to represent:
.foo?.foo[expr]?.[expr]
The exact key design is still open. It only needs to support dedupe among
captures that are semantically comparable.
In src/transforms/scope.ts,
getOutermostBindableReference() currently accumulates only plain
non-computed member chains and stops at unsupported hops.
To support optional/computed access, capture analysis must preserve richer
member-hop metadata instead of reducing everything to Identifier or
MemberExpression with plain identifier-name segments.
That likely means changing either:
- what
referenceToNodestores, or - adding a new structured capture representation derived from the AST
In src/transforms/hoist.ts,
memberExpressionToPath() currently extracts only string[] segments.
That helper would need to become a structured extractor that records:
- property vs computed
- optional vs non-optional
- enough information to regenerate the bind expression
Current prefix dedupe is straightforward for plain dot paths:
x.ycoversx.y.zxcovers everything below it
With optional/computed access, dedupe needs clearer rules.
Questions:
- does
x.ycoverx.y?.z? - does
x[y]coverx[y].zonly when the computed key expression is identical? - how should keys be normalized for comparison?
The current antichain logic should not be reused blindly.
This is still intentionally unresolved.
Possible support levels:
-
Keep current safe-prefix bailout only. Examples:
obj[key]-> bindobj, bindkeyseparately if it is an outer captureobj[key].value-> bindobj, bindkeyseparately if needed
-
Support exact computed member captures only for simple shapes. Examples:
obj[key]obj[key].valuebut only when we have a clear representation for both the base object and the key expression.
-
Support computed access as a first-class bind path. This would require fully defining:
- path equality
- prefix coverage
- codegen for bind expressions
- partial-object synthesis, if still applicable
At the moment, the note does not assume we will reach (3). It is entirely reasonable to stop at (1) or (2) if the semantics and implementation cost of full computed-path support are not compelling.
Current codegen only needs:
rootsegments: string[]
and synthesizes:
root + segments.map((segment) => `.${segment}`).join('')That must be replaced with codegen that can emit:
.foo?.foo[expr]?.[expr]
This is the hardest part.
For plain member paths, partial-object synthesis is natural:
{
y: {
z: x.y.z
}
}For computed access, synthesis is less obvious:
x[k].zQuestions:
- should this become an object with computed keys?
- should computed paths fall back to broader binding even after we support recognizing them?
- does partial-object binding remain the right representation for these cases?
This is where the design may need to diverge from plain member chains.
Relevant prior art is documented in scope-manager-research/nextjs.md.
Important comparison points:
- Next.js already models optional member access in its
NamePartstructure. - Next.js does not support computed properties in the captured member-path model.
- Next.js member-path capture is deliberately limited to member chains like
foo.bar.baz.
That means:
- optional chaining has direct prior art in Next.js's capture model
- computed access does not; if we support it, we are going beyond the current Next.js design
This should affect scoping decisions for the follow-up:
- optional support is an extension of an already-established member-path model
- computed support is a materially larger design question, especially once key expression scope and dedupe semantics are included
If we want a minimal correctness-first follow-up:
- keep the current safe-prefix bailout behavior
- add explicit tests for optional/computed cases
- only implement richer capture metadata once codegen and dedupe rules are agreed
That avoids regressing semantics while leaving room for a more precise design.
Current working direction:
- likely support optional chaining next, to align with Next.js's existing member-path behavior
- keep computed access as a separate, open design problem for now
Rationale:
- optional chaining already has prior art in Next.js's capture model
- computed access is materially more complex because it mixes:
- key-expression scope resolution
- path equality / dedupe rules
- bind-expression codegen
- unclear partial-object synthesis semantics
So the likely near-term path is:
- support optional member chains
- keep current conservative behavior for computed access
- revisit computed support only if there is a clear use case and a concrete design that handles key-expression closure semantics correctly
-
Optional chains: Should the first supported version preserve optional syntax exactly in the bound expression, or should optional hops continue to bail out?
-
Computed access: Do we want exact support for
x[y].z, or only a less coarse bailout than binding the whole root? -
Binding shape: Is partial-object synthesis still the preferred strategy for computed access, or does this push us toward a different representation?
-
Computed key scope: If we support
obj[expr], what is the intended contract for the key expression? Specifically:- must outer variables used in
expralways be captured independently? - do we need a representation that distinguishes outer
keyfrom action-localkeywhen deciding support and dedupe?
- must outer variables used in
-
Comparison target: Do we want to stay aligned with Next.js and continue treating computed access as out of scope, or intentionally support a broader feature set?
Add focused hoist fixtures for:
x?.y.zx.y?.zx?.y?.zx[y].zx.y[k]x[y]?.za.b?.cas a safe-prefix bailout baselinea[b].cas a safe-prefix bailout baseline