Mapped collections
Turn an array or record signal into a stable set of per-item derivations, so each item stays independently reactive across changes to the list.
@mmstack/primitivesnpm
#The problem they solve
You have a signal holding an array, and you want a derived value per item: a formatted label, a small view model, a per-row control. The obvious move is a computed that maps over the array.
That works, but it throws everything away and rebuilds it on every change. Change one item's name and the whole mapped array is recreated, which means fresh object references for every row. Anything downstream keyed on those references, a heavy component in a @for, a chart, a drag handle, sees every row as new and re-renders the lot. There is also no stable per-item signal to hand to a child, since each pass produces new objects.
These three helpers fix that. Each one maps a source array or record into a set of derived values that are created once per item and reused as the source changes. Change one item and only that item's derivation updates; the rest keep their identity. That gives you a stable per-item signal to pass down, and it lets change detection skip the rows that did not move.
indexArraystabilizes an array by position.keyArraystabilizes an array by identity.mapObjectis the record equivalent, stable by key.
They also pool their internal buffers, so reordering a large list is much cheaper than mapping a computed.
#indexArray, the default
indexArray is the one to reach for first. It maps the source into a stable array where each position gets its own writable child signal holding the item at that index. The mapping runs once per position and is reused as the array changes, so a growing list only creates entries for the new tail.
import { computed } from '@angular/core';
import { indexArray, mutable } from '@mmstack/primitives';
const items = mutable([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
]);
// child is a MutableSignal<{ id, name }> for the current index
const labels = indexArray(items, (child, index) =>
computed(() => `Item ${index}: ${child().name}`),
);Because it is keyed by position, a mapped output follows the position, not the item. If the array reorders, position 0 keeps its derivation and its child signal simply reports the new item there. That is exactly what you want for a plain list of labels, and it is the cheaper of the two array helpers.
If you know it from SolidJS, mapArray is a deprecated alias for indexArray; it stays for compatibility, so reach for indexArray in new code.
#keyArray, stable by identity
keyArray stabilizes by identity instead. You pass a key selector; moving an item then carries its derivation along, and only the item's index signal updates. Here the mapping receives the item value and a Signal<number> for its current index.
import { computed } from '@angular/core';
import { keyArray, mutable } from '@mmstack/primitives';
const items = mutable([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
]);
// child is the item value, index is a Signal<number>
const keyed = keyArray(
items,
(child, index) => computed(() => `${index()}: ${child.name}`),
{ key: (item) => item.id },
); The difference matters when an item owns something expensive that should survive a reorder: a rendered chart, a media element with playback state, a drag-and-drop row. With indexArray a reorder would hand each position a different item and force those to rebuild; with keyArray the instance moves with its item. If nothing downstream depends on instance reuse across reorders, indexArray is enough. Both accept an onDestroy callback that runs when a mapped entry is removed, so per-item resources can clean up.
#mapObject for records
mapObject is the record counterpart of keyArray. It maps a Record<K, V> into a Record<K, U> with referential stability for unchanged keys, and when the source is writable it hands the mapping a writable signal slice for each value, so each entry can both read and write its own value.
import { signal } from '@angular/core';
import { mapObject } from '@mmstack/primitives';
const settings = signal<Record<string, boolean>>({
wifi: true,
bluetooth: false,
});
const controls = mapObject(
settings,
(key, value) => ({
label: key.toUpperCase(),
isActive: value, // WritableSignal<boolean>
toggle: () => value.update((v) => !v),
}),
{ onDestroy: (entry) => console.log(`Removed ${entry.label}`) },
); The result is a set of self-contained controls, one per key, that stay the same objects until their key leaves the record. As with the array helpers, onDestroy runs when a key is removed.