DocsRenderGuidesState Store

State Store

The state store holds the reactive state that drives your rendered UI. @ngaf/render provides signalStateStore(), an Angular Signals-backed implementation of the StateStore interface from @json-render/core.

Creating a State Store

import { signalStateStore } from '@ngaf/render';
 
const store = signalStateStore({
  user: { name: 'Alice', age: 30 },
  items: ['apple', 'banana', 'cherry'],
  isVisible: true,
});

The function accepts an optional initial state object (defaults to {}). It returns a StateStore that uses Angular Signals internally, so any state changes automatically trigger Angular's change detection.

JSON Pointer Paths

All state access uses JSON Pointer paths. A JSON Pointer is a string that identifies a specific value within a JSON document.

PathResolves to
/user/name'Alice'
/user/age30
/items/0'apple'
/items/2'cherry'
/isVisibletrue

Paths always start with /. Each segment separated by / traverses one level deeper into the object. Array elements are accessed by index.

Escaping

JSON Pointer defines two escape sequences for special characters in property names:

  • ~0 represents ~
  • ~1 represents /

For example, to access a property named a/b, the pointer would be /a~1b.

Reading State

Use get() to read a value at a path:

const store = signalStateStore({ user: { name: 'Alice' } });
 
store.get('/user/name');  // 'Alice'
store.get('/user');       // { name: 'Alice' }
store.get('/missing');    // undefined

Writing State

Single Value

Use set() to write a single value. The store performs an immutable update -- it clones the path to the target and sets the new value. If the new value is referentially equal to the current value, the update is skipped.

store.set('/user/name', 'Bob');
store.get('/user/name'); // 'Bob'

Batch Updates

Use update() to set multiple values in a single operation. This triggers only one notification to subscribers, regardless of how many values change.

store.update({
  '/user/name': 'Charlie',
  '/user/age': 25,
  '/isVisible': false,
});

If none of the values actually change (all are referentially equal), no notification is triggered.

Snapshots

Use getSnapshot() to get the entire state object:

const store = signalStateStore({ x: 1, y: 2 });
store.getSnapshot(); // { x: 1, y: 2 }
 
store.set('/x', 10);
store.getSnapshot(); // { x: 10, y: 2 }

Subscribing to Changes

Use subscribe() to register a callback that is invoked whenever the state changes. The function returns an unsubscribe function.

const store = signalStateStore({ count: 0 });
 
const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getSnapshot());
});
 
store.set('/count', 1); // logs: State changed: { count: 1 }
store.set('/count', 2); // logs: State changed: { count: 2 }
 
unsubscribe(); // stop listening
store.set('/count', 3); // no log -- unsubscribed

Reactive Behavior with Angular Signals

Under the hood, signalStateStore() wraps the state in an Angular signal(). This means:

  • Components using OnPush change detection automatically update when the state changes
  • Props resolved via $state expressions in specs are re-evaluated when the underlying signal updates
  • The store integrates seamlessly with Angular's reactivity model -- no RxJS or manual subscription management needed
// In a spec, $state props are automatically reactive
{
  type: 'Text',
  props: {
    label: { $state: '/user/name' },  // re-evaluated on state change
  },
}

Working with Arrays

The store preserves array types when updating elements by index:

const store = signalStateStore({ items: ['a', 'b', 'c'] });
 
store.set('/items/1', 'B');
store.get('/items/1');    // 'B'
store.get('/items');      // ['a', 'B', 'c']
 
// The items value is still an array
Array.isArray(store.get('/items')); // true

Providing the Store

You can provide a store in three ways. RenderSpecComponent resolves the store using this priority chain:

1
Input (highest priority)

Pass a store directly to <render-spec>:

<render-spec [spec]="spec" [store]="store" />
2
Global config (via provideRender)

Set a store in provideRender():

provideRender({
  registry: myRegistry,
  store: signalStateStore({ theme: 'dark' }),
})
3
Internal (from spec.state)

If no external store is provided, RenderSpecComponent creates an internal signalStateStore() from spec.state:

const spec: Spec = {
  root: 'root',
  elements: { /* ... */ },
  state: { message: 'Hello' }, // used to create an internal store
};

Next Steps