Core Concepts
Member expressions
Member paths are a core part of rs-x: they resolve nested values across objects, arrays, maps, sets, function calls, and async segments.
Practical value
Key points
- Path evaluation is segment-based and left-to-right, with each segment using the previous segment value as context.
- When a segment in the path changes, rs-x rebinds dependent segments in order. For example, with `a.b.c.d`, if `b` is replaced, rs-x first rebinds `c` to the new `b`, then rebinds `d` to the new `c`.
- Computed member slots (like `tasks[trackedTask]`) are rebound when their key expression changes, without rebuilding the whole expression tree.
- Array/Map/Set members are resolved by dedicated owner resolvers and index accessors, not by ad-hoc branch logic.
- Nested Promise/Observable paths are supported and continue automatically when values become available.
- Reevaluation clears and recomputes only the affected part of the path.
How a member path is represented and evaluated
The parser flattens member syntax into path segments. For example, `a.b[c].d` becomes ordered segments where computed segments are represented as index expressions. Evaluation then runs segment-by-segment, always carrying forward the previous segment result as the context for the next lookup.
This keeps path semantics explicit. A change in an early segment invalidates only what depends on it, instead of forcing full expression recomputation.
When a segment changes, rs-x rebinds dependent segments in order. For example, with `a.b.c.d`, if `b` is replaced, rs-x first rebinds `c` to the new `b`, then rebinds `d` to the new `c`.
Bind flow and deferred initialization
During bind, root and calculated segments are bound from the original root context, while non-root path segments wait for the previous segment value. MemberExpression queues late binds with `queueMicrotask` so a bind side effect cannot trigger nested evaluation in the same call stack.
That deferred bind step prevents re-entrancy issues and gives a stable evaluation order, especially for long paths with mixed sync/async segments.
Dynamic computed slots: prices[key], tasks[trackedTask]
For computed members, rs-x resolves the current index value first, then creates an internal static slot observer for that specific resolved key. If the dynamic key changes later, the previous slot observer is disposed and rebound to the new key.
This rebinding flow is what keeps expressions like `map[currentKey]` or `set[selectedItem]` reactive to both key changes and value/membership changes for the currently selected slot.
Owner resolution across object, array, map, and set
Identifier owner resolution is delegated to resolver chain services. Property owners, array indexes, set membership keys, and map keys each have explicit resolver logic. That means member paths do not rely on one generic fallback for all container types.
For Set and Map, key identity matters. Paths like `tasks[trackedTask]` and `prices["pro"]` are resolved against membership/keys directly, then the value path continues from that resolved member.
Async segments in the middle of a member path
Nested async is useful when one async value depends on another async value that must be resolved first.
A common pattern is: fetch A first, then use A to fetch or subscribe to B.
When a segment resolves to a Promise or Observable, rs-x keeps the path in a waiting state until the value is available, then continues the rest of the path automatically.
Nested async paths such as `{ x: Promise.resolve({ y: Promise.resolve(20) }) }` with `x.y`, or Observable equivalents, are handled as normal member paths once each layer becomes available.
Change propagation and transaction integration
Identifier segments report state changes through IndexValueObserver, then register commits with the transaction manager. MemberExpression `prepareReevaluation` clears only non-calculated path parts after that point that are no longer valid for the new value.
In practice this gives precise updates: only affected tracked paths are reevaluated and committed, which reduces noisy work and keeps emitted changes aligned with the semantic path that changed.
Function example
Call a function inside the member path (`cart.first().qty`) and keep property tracking after the function call reactive.
import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';
await InjectionContainer.load(RsXExpressionParserModule);
const model = {
cart: {
items: [
{ id: 'A', qty: 1 },
{ id: 'B', qty: 2 },
],
first() {
return this.items[0];
},
},
};
const firstQtyExpression = rsx<number>('cart.first().qty')(model);
await new WaitForEvent(firstQtyExpression, 'changed').wait(emptyFunction);
const changed = await new WaitForEvent(firstQtyExpression, 'changed', {
ignoreInitialValue: true,