Guide

Collections

Array, Map, and Set are reactive in rs-x expressions.

What this means in practice

Collections can be watched at two levels: the full collection expression (for example cart) or a selected entry expression (for example cart[0]).

Full collection expressions react to collection mutations like push, set, add, delete, and clear. Selected entry expressions react to that specific index/key/member. Nested properties inside the selected entry are only tracked when you pass an IndexWatchRule.

How collection updates flow

  • Watch an array property (for example cart) to react to mutations like push/pop/splice.
  • Watch a map property (for example prices) to react to mutations like set/delete/clear.
  • Watch a set property (for example tasks) to react to mutations like add/delete/clear.
  • Watch items[0], roles["admin"], or tasks[trackedTask] to react to one selected entry.
  • Use IndexWatchRule when you also need nested property tracking inside that selected entry.

Behavior by collection type

Array: rs-x reacts when an item is changed, moved, added, or removed. This is useful for cart rows, table data, and ordered steps in workflows.

Map: rs-x reacts at key level. It can pick up updates when a key is added, replaced, or deleted. This is useful when your model is keyed by ids, names, or roles.

Set: rs-x reacts to membership changes and can also react to tracked member details. This is useful for selections, tags, and active item groups.

When to watch full collection vs item

Use a full collection expression when you care about collection mutations. Example: cart reacts to array operations like push/splice, prices reacts to map set/delete, and tasks reacts to set add/delete.

Use an item/key/member expression when you need to observe one selected entry, not the whole collection. This keeps updates focused on the selected branch and avoids reacting to unrelated entries. Add an IndexWatchRule when the selected entry must also react to nested fields (for example done or qty).

  • Use a full collection expression for structural mutations: add/remove/replace entries.
  • Use an item/key/member expression when only one selected entry matters.
  • Add IndexWatchRule only when nested fields inside that selected entry must trigger updates.

Examples

These examples watch one selected entry path and show when nested changes require IndexWatchRule.

Array: non-recursive (default)

By default, rs-x tracks the expressed array leaf only. Nested property changes on that leaf are ignored unless you replace the expressed item itself.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';

await InjectionContainer.load(RsXExpressionParserModule);

const model = {
  cart: [
    { id: 'A', qty: 1 },
    { id: 'B', qty: 2 },
  ],
};

// Default: non-recursive leaf watching
const firstItemExpression = rsx('cart[0]')(model);

await new WaitForEvent(firstItemExpression, 'changed').wait(emptyFunction);

// Not tracked by default (nested leaf property)
const nestedChange = await new WaitForEvent(firstItemExpression, 'changed', {
  ignoreInitialValue: true,
  timeout: 100,
}).wait(() => {

Array: recursive (with IndexWatchRule)

This example turns on recursive leaf watching for cart[0].qty by passing an IndexWatchRule.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';
import { IndexWatchRule } from '@rs-x/state-manager';

await InjectionContainer.load(RsXExpressionParserModule);

const model = {
  cart: [
    { id: 'A', qty: 1, note: 'tracked item' },
    { id: 'B', qty: 5, note: 'ignored item' },
  ],
};

const watchRule = new IndexWatchRule(model, (index, target, rootModel) => {
  if (target === rootModel && index === 'cart') {
    return true;
  }

  if (Array.isArray(target)) {
    return Number(index) === 0;
  }

Map: non-recursive (default)

By default, rs-x tracks the expressed map key leaf itself. Nested property changes on that key value are ignored.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';

await InjectionContainer.load(RsXExpressionParserModule);

const model = {
  prices: new Map([
    ['admin', { enabled: true, note: 'leaf object' }],
    ['guest', { enabled: false, note: 'other object' }],
  ]),
};

// Default: non-recursive leaf watching
const adminExpression = rsx('prices["admin"]')(model);
await new WaitForEvent(adminExpression, 'changed').wait(emptyFunction);

const nestedChange = await new WaitForEvent(adminExpression, 'changed', {
  ignoreInitialValue: true,
  timeout: 100,
}).wait(() => {
  const admin = model.prices.get('admin');
  if (admin) {

Map: recursive (with IndexWatchRule)

This example keeps watching the admin key branch recursively and reacts to the enabled property using IndexWatchRule.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';
import { IndexWatchRule } from '@rs-x/state-manager';

await InjectionContainer.load(RsXExpressionParserModule);

const admin = { enabled: true, note: 'tracked leaf' };
const guest = { enabled: false, note: 'other leaf' };

const model = {
  roles: new Map([
    ['admin', admin],
    ['guest', guest],
  ]),
};

const watchRule = new IndexWatchRule(model, (index, target, rootModel) => {
  if (target === rootModel && index === 'roles') {
    return true;
  }

  if (target instanceof Map) {

Set: non-recursive (default)

By default, rs-x tracks the expressed set member leaf itself. Nested property changes on that member are ignored.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';

await InjectionContainer.load(RsXExpressionParserModule);

const taskA = { id: 'A', done: false, note: 'leaf object' };
const taskB = { id: 'B', done: false, note: 'other object' };

const model = {
  trackedTask: taskA,
  tasks: new Set([taskA, taskB]),
};

// Default: non-recursive leaf watching
const trackedTaskExpression = rsx('tasks[trackedTask]')(model);
await new WaitForEvent(trackedTaskExpression, 'changed').wait(emptyFunction);

const nestedChange = await new WaitForEvent(trackedTaskExpression, 'changed', {
  ignoreInitialValue: true,
  timeout: 100,
}).wait(() => {
  taskA.done = true;

Set: recursive (with IndexWatchRule)

This example enables recursive watching for one member and one nested property via IndexWatchRule.

import { emptyFunction, InjectionContainer, WaitForEvent } from '@rs-x/core';
import { rsx, RsXExpressionParserModule } from '@rs-x/expression-parser';
import { IndexWatchRule } from '@rs-x/state-manager';

await InjectionContainer.load(RsXExpressionParserModule);

const taskA = { id: 'A', done: false, note: 'tracked member' };
const taskB = { id: 'B', done: false, note: 'ignored member' };

const model = {
  trackedTask: taskA,
  tasks: new Set([taskA, taskB]),
};

const isTrackedTask = (candidate: unknown, tracked: { id: string }) => {
  if (candidate === tracked) {
    return true;
  }

  return (
    typeof candidate === 'object' &&
    candidate !== null &&