Skip to main content

Manage entities with RxState

Managing a collection in component state means writing the same add / update / remove plumbing over and over: find the item in the array, rebuild the array immutably, put it back. RxAngular ships that plumbing as a set of pure, immutable helpers in @rx-angular/cdk/transformationsinsert, update, upsert, remove, and the toDictionary / dictionaryToArray pair. Each returns a new array (or object) and never mutates its input, so it drops straight into an rxState container: the component holds almost no collection code, and you pull in no entity library to get it.

Goal / when to use

Use this recipe whenever a component owns a list of keyed records — todos, users, cart lines, search results — and needs to add, change, and drop them reactively. The helpers key into the collection by a field you name ('id'), a list of fields, or a custom comparator, so they fit any shape of record.

Setup: the state shape

Keep the collection as a plain array:

interface Item {
id: string;
name: string;
}

interface ComponentState {
items: Item[];
loading: boolean;
}

Add, update, and remove

Connect each user action to a helper inside the rxState initializer. The three-argument connect(key, source$, (state, value) => newValue) overload hands you the current slice and takes back the next one:

import { Component } from '@angular/core';
import { rxState } from '@rx-angular/state';
import { insert, update, remove } from '@rx-angular/cdk/transformations';
import { Subject } from 'rxjs';

@Component({ selector: 'my-component' })
export class MyComponent {
readonly addItem$ = new Subject<Item>();
readonly updateItem$ = new Subject<Item>();
readonly removeItem$ = new Subject<Item>();

private readonly state = rxState<ComponentState>(({ set, connect }) => {
set({ items: [], loading: false });
connect('items', this.addItem$, ({ items }, item) => insert(items, item));
connect('items', this.updateItem$, ({ items }, item) => update(items, item, 'id'));
connect('items', this.removeItem$, ({ items }, item) => remove(items, item, 'id'));
});

readonly items = this.state.signal('items');
}
  • insert(items, itemOrItems) — append one item, or several at once.
  • update(items, item, 'id') — replace the item whose id matches. The third argument can be a key, a list of keys (['id', 'name']), or a comparator (a, b) => a.id === b.id.
  • remove(items, item, 'id') — drop matching items. You can also remove by raw value when the array holds primitives, e.g. remove(ids, ['a', 'b']).

For an action that should insert when the record is new and replace when it already exists, use upsert:

import { upsert } from '@rx-angular/cdk/transformations';

connect('items', this.saveItem$, ({ items }, item) => upsert(items, item, 'id'));

Imperative updates

Outside a stream, run the same helpers through set() — read the current slice with get(), transform it, and write it back:

addItem(item: Item): void {
this.state.set({ items: insert(this.state.get('items'), item) });
}
get() is a snapshot, not a reactive read

get() returns a one-shot, non-reactive snapshot: it creates no dependency and never re-runs when state changes, so reach for it only in imperative code like event handlers and set()/connect() updater callbacks. For anything rendered or derived, read reactively with signal('key'), select(...), or computed(...).

Fast lookups: normalize to a dictionary

When you read records by id far more often than you iterate them, hold the collection as a keyed dictionary (Record<string, Item>) instead of an array. toDictionary builds one from an array; dictionaryToArray turns it back for rendering:

import { computed } from '@angular/core';
import { toDictionary, dictionaryToArray } from '@rx-angular/cdk/transformations';

// build a lookup from an incoming array
connect('itemsById', this.loadItems$, (_, items) => toDictionary(items, 'id'));

readonly itemsById = this.state.signal('itemsById');
// O(1) read by id
getItem = (id: string): Item | undefined => this.itemsById()[id];
// materialize a list only where you iterate
readonly itemList = computed(() => dictionaryToArray(this.itemsById()));

Once normalized, update or add a field on a single record by key with patch / setProp, and drop one with deleteProp.

Selecting and rendering

state.signal('items') exposes the collection as a Signal<Item[]> for direct template binding; state.select('items') gives the Observable<Item[]> when you need to compose it further. Derived views are plain reads:

readonly items = this.state.signal('items');
readonly total = computed(() => this.items().length);

Result

Add / update / remove and normalization all come from small pure functions, so the component carries no bespoke collection logic and no entity-library dependency — just @rx-angular/state for the container and @rx-angular/cdk/transformations for the moves.

See also