What It Controls The rule is evaluated with test(index, target) whenever rs-x decides if a nested value should remain observed. Returning true keeps recursive observation active for that member path.
Without a watch rule, rs-x still tracks root assignments and collection membership mutations. But nested member/property changes under leaf values are only tracked when the rule allows them.
Where To Pass The Rule Use it either at expression binding time (leafIndexWatchRule ) or directly in state manager (watchState(..., { indexWatchRule }) ).
import { rsx } from '@rs-x/expression-parser' ;
import type { IIndexWatchRule } from '@rs-x/state-manager' ;
const watchRule : IIndexWatchRule = {
context : { allow : new Set ( [ 'a' , 'b' ] ) } ,
test ( index ) {
return this . context . allow . has ( String ( index ) ) ;
} ,
} ;
const model = { a : 1 , b : 2 , c : 3 } ;
const expression = rsx < number > ( 'a + b' ) ( model , watchRule ) ; import type { IStateManager } from '@rs-x/state-manager' ;
const watchRule = {
context : { recursive : true } ,
test ( _index , _target ) {
return this . context . recursive ;
} ,
} ;
stateManager . watchState ( model , 'user' , { indexWatchRule : watchRule } ) ; Parameters context unknown
User - defined data bag used by test (...) to hold rule config .
index unknown
Current member / index candidate under evaluation .
target unknown
Object / collection that owns the current index / member .
Return Type test(...) returns boolean .
Return true to include a member in recursive observation; return false to skip it.
Index / Target Semantics By Type The meaning of index depends on runtime type:
Practical Pattern The usual rule shape is:
Allow the leaf container itself (for example model.items ) so recursive observation can be installed. Allow specific collection members/keys or nested object properties. Keep rule logic deterministic and side-effect free. import { rsx } from '@rs-x/expression-parser' ;
import { watchIndexRecursiveRule } from '@rs-x/state-manager' ;
const model = { a : { b : { c : 1 } } } ;
const expression = rsx ( 'a.b' ) ( model , watchIndexRecursiveRule ) ; Plain Object Example Tracks only user.profile.name while ignoring user.profile.role.
const rsx = api . rsx ;
const model = {
user : {
profile : { name : 'Ada' , role : 'engineer' } ,
} ,
} ;
const watchRule = {
context : { trackedLeafProperties : new Set ( [ 'name' ] ) } ,
test ( index , target ) {
if ( target == = model && index == = 'user' ) {
return true ;
}
if ( target == = model . user && index == = 'profile' ) {
return true ;
}
if ( target == = model . user . profile ) {
return this . context . trackedLeafProperties . has ( String ( index ) ) ;
} Show full code
Date Example Tracks schedule.start hours/minutes updates and ignores seconds.
const rsx = api . rsx ;
const model = {
schedule : {
start : new Date ( 2026 , 0 , 1 , 9 , 30 , 0 ) ,
} ,
} ;
const watchRule = {
context : { trackedDateParts : new Set ( [ 'hours' , 'minutes' ] ) } ,
test ( index , target ) {
if ( target == = model && index == = 'schedule' ) {
return true ;
}
if ( target == = model . schedule && index == = 'start' ) {
return true ;
}
if ( target == = model . schedule . start ) {
return this . context . trackedDateParts . has ( String ( index ) ) ;
} Show full code
Array Example Tracks array slot mutations and qty changes on each item, but not note.
const rsx = api . rsx ;
const model = {
items : [
{ label : 'A' , qty : 1 , note : 'keep' } ,
{ label : 'B' , qty : 2 , note : 'keep' } ,
] ,
} ;
const watchRule = {
context : { trackedItemProperty : 'qty' } ,
test ( index , target ) {
if ( target == = model && index == = 'items' ) {
return true ;
}
if ( Array . isArray ( target ) ) {
return true ;
}
return String ( index ) == = this . context . trackedItemProperty ;
} , Show full code
Map Example Tracks only the admin key branch and enabled property changes.
const rsx = api . rsx ;
const WaitForEvent = api . WaitForEvent ;
const emptyFunction = ( ) => { } ;
const admin = { enabled : true , rank : 1 } ;
const guest = { enabled : false , rank : 2 } ;
const model = {
roles : new Map ( [
[ 'admin' , admin ] ,
[ 'guest' , guest ] ,
] ) ,
} ;
const watchRule = {
context : { trackedKeys : new Set ( [ 'admin' ] ) } ,
test ( index , target ) {
if ( target == = model && index == = 'roles' ) {
return true ;
}
if ( target instanceof Map ) { Show full code
Set Example Tracks a set member path by default (non-recursive): nested done is ignored, membership changes are tracked.
const rsx = api . rsx ;
const WaitForEvent = api . WaitForEvent ;
const emptyFunction = ( ) => { } ;
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 ] ) ,
} ;
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 ;
} ) ; Show full code
export interface IIndexWatchRule {
context : unknown ;
test ( index : unknown , target : unknown ) : boolean ;
}