ObservableArray change events: the action table
ObservableArray emits one change event per mutation with ChangedData<T>: { action, index, addedCount, removed }. The four actions are the string literals 'add', 'delete', 'update', 'splice'. A handler that only switches on 'add'/'delete' silently misses every length = mutation — those are 'splice'.
import { ChangedData, ObservableArray } from '@nativescript/core';
const items = new ObservableArray(['a', 'b', 'c']);
items.on(ObservableArray.changeEvent, (args: ChangedData<string>) => {
if (args.action === 'splice') {
rebuildAll(); // covers items.length = N in both directions
} else {
patchRows(args.index, args.addedCount, args.removed);
}
});
items.length = 0; // -> action 'splice', index 0, addedCount 0, removed ['a','b','c']
What each mutation reports
| Mutation |
action |
index |
addedCount |
removed |
push(x) / push(x, y, z) |
add |
length before the push |
count pushed |
[] |
pop() |
delete |
new length |
0 |
[last] |
shift() |
delete |
0 |
0 |
[first] |
unshift(x) |
add (not a distinct action) |
0 |
count |
[] |
splice(i, del, ...add) |
splice |
i clamped to current length |
added |
deleted items |
setItem(i, v) |
update |
i |
1 |
[old value] |
length = n (shrink or grow) |
splice |
old length (grow) / n (shrink) |
grown count |
shrunk items |
Details the table can't show:
sort() and reverse() emit NOTHING. Both mutate in place and skip notify() entirely — a bound ListView keeps the old order until you force it. Re-sort with items.splice(0, items.length, ...sorted) (one 'splice' event) or call listView.refresh() after sorting.
- No bracket access.
arr[0] is undefined and arr[0] = x fires nothing — use getItem(i) / setItem(i, v) (both accept negative indices).
length = is always 'splice'. Growing [1,2,3] with length = 5 fires { action: 'splice', index: 3, addedCount: 2, removed: [] }.
- Never retain the
args object. The 'add'/'delete' event args are shared mutable singletons reused by every push/pop/shift/unshift — copy the fields you need inside the handler.
splice clamps its start index: on a 1-item array, array.splice(2, 0, x) reports index: 1 (normalized to the array end), not 2.
map/filter/slice/concat return ObservableArray, not plain arrays.
ChangeType.Change exists as a constant but is never emitted by any method — don't wait for it.
VirtualArray disagrees: on a VirtualArray, array.length += array.loadSize fires 'add', not 'splice' — don't share one handler between the two types blindly.
Constructor arity trap
new ObservableArray(1, 2, 3); // 3 items: [1, 2, 3]
new ObservableArray(100); // NOT one item — an array of length 100
new ObservableArray([100]); // one item
A single numeric argument means "length", matching Array. When building from a variable that might be a lone number, always pass an array literal.
Practical rules
- Prefer
ObservableArray mutations over re-assigning a plain array to items on list controls — ListView applies the change incrementally instead of re-rendering (see ns-listview-recycling).
removed on 'update' is how you diff: it holds the value being replaced.
- To reset,
array.length = 0 (one 'splice') beats popping in a loop (N events).
Verified 2026-08 against @nativescript/core 9.1.0-rc.3: behaviors asserted in the framework's test suite (apps/automated/src/data/observable-array-tests.ts, virtual-array-tests.ts) and confirmed in source (packages/core/data/observable-array/index.ts — sort/reverse notify-free, shared _addArgs/_deleteArgs singletons); not re-run standalone.
1---2name: ns-observable-array-change-events3description: Use when handling ObservableArray change events (on('change', ...), ChangedData, args.action/index/addedCount/removed) or when a list misses updates after array.length = n — length assignments emit 'splice' (never add/delete), push reports the pre-push index, setItem's removed holds the old value, and the ObservableArray constructor has a numeric-arity trap.4license: Apache-2.05---67# ObservableArray change events: the action table89`ObservableArray` emits one `change` event per mutation with `ChangedData<T>`: `{ action, index, addedCount, removed }`. The four actions are the string literals `'add'`, `'delete'`, `'update'`, `'splice'`. A handler that only switches on `'add'`/`'delete'` silently misses every `length =` mutation — those are `'splice'`.1011```ts12import { ChangedData, ObservableArray } from '@nativescript/core';1314const items = new ObservableArray(['a', 'b', 'c']);15items.on(ObservableArray.changeEvent, (args: ChangedData<string>) => {16 if (args.action === 'splice') {17 rebuildAll(); // covers items.length = N in both directions18 } else {19 patchRows(args.index, args.addedCount, args.removed);20 }21});22items.length = 0; // -> action 'splice', index 0, addedCount 0, removed ['a','b','c']23```2425## What each mutation reports2627| Mutation | action | index | addedCount | removed |28|---|---|---|---|---|29| `push(x)` / `push(x, y, z)` | `add` | length *before* the push | count pushed | `[]` |30| `pop()` | `delete` | new length | 0 | `[last]` |31| `shift()` | `delete` | 0 | 0 | `[first]` |32| `unshift(x)` | `add` (not a distinct action) | 0 | count | `[]` |33| `splice(i, del, ...add)` | `splice` | i clamped to current length | added | deleted items |34| `setItem(i, v)` | `update` | i | 1 | `[old value]` |35| `length = n` (shrink **or** grow) | `splice` | old length (grow) / n (shrink) | grown count | shrunk items |3637Details the table can't show:3839* **`sort()` and `reverse()` emit NOTHING.** Both mutate in place and skip `notify()` entirely — a bound ListView keeps the old order until you force it. Re-sort with `items.splice(0, items.length, ...sorted)` (one `'splice'` event) or call `listView.refresh()` after sorting.40* **No bracket access.** `arr[0]` is `undefined` and `arr[0] = x` fires nothing — use `getItem(i)` / `setItem(i, v)` (both accept negative indices).41* **`length =` is always `'splice'`.** Growing `[1,2,3]` with `length = 5` fires `{ action: 'splice', index: 3, addedCount: 2, removed: [] }`.42* **Never retain the `args` object.** The `'add'`/`'delete'` event args are shared mutable singletons reused by every push/pop/shift/unshift — copy the fields you need inside the handler.43* **`splice` clamps its start index**: on a 1-item array, `array.splice(2, 0, x)` reports `index: 1` (normalized to the array end), not 2.44* **`map`/`filter`/`slice`/`concat` return `ObservableArray`**, not plain arrays.45* **`ChangeType.Change` exists as a constant but is never emitted** by any method — don't wait for it.46* **`VirtualArray` disagrees**: on a `VirtualArray`, `array.length += array.loadSize` fires `'add'`, not `'splice'` — don't share one handler between the two types blindly.4748## Constructor arity trap4950```ts51new ObservableArray(1, 2, 3); // 3 items: [1, 2, 3]52new ObservableArray(100); // NOT one item — an array of length 10053new ObservableArray([100]); // one item54```5556A single numeric argument means "length", matching `Array`. When building from a variable that might be a lone number, always pass an array literal.5758## Practical rules5960* Prefer `ObservableArray` mutations over re-assigning a plain array to `items` on list controls — `ListView` applies the change incrementally instead of re-rendering (see `ns-listview-recycling`).61* `removed` on `'update'` is how you diff: it holds the value being replaced.62* To reset, `array.length = 0` (one `'splice'`) beats popping in a loop (N events).6364Verified 2026-08 against @nativescript/core 9.1.0-rc.3: behaviors asserted in the framework's test suite (apps/automated/src/data/observable-array-tests.ts, virtual-array-tests.ts) and confirmed in source (packages/core/data/observable-array/index.ts — sort/reverse notify-free, shared _addArgs/_deleteArgs singletons); not re-run standalone.