Skip to content

Observable Array (createObservableArray)

createObservableArray<T> (from scenerystack/axon) builds an object with the exact same API as a native JavaScript Array<T> — you can push, pop, splice, index with [i], iterate, etc. — but that also notifies listeners whenever its contents change. Internally it wraps a real array in a Proxy and exposes an Emitter for additions, an Emitter for removals, and a NumberProperty tracking the length.

ts
import { createObservableArray } from 'scenerystack/axon';

const particles = createObservableArray<{ id: number }>();

particles.elementAddedEmitter.addListener( particle => console.log( 'added', particle.id ) );
particles.elementRemovedEmitter.addListener( particle => console.log( 'removed', particle.id ) );
particles.lengthProperty.link( length => console.log( 'count:', length ) );

particles.push( { id: 1 } ); // logs "added 1", then "count: 1"
particles.pop();             // logs "removed 1", then "count: 0"

lengthProperty always updates before the corresponding elementAddedEmitter/elementRemovedEmitter fires, so a listener on either emitter can safely read the up-to-date lengthProperty.value.

Observable members

MemberTypeNotes
elementAddedEmitterTEmitter<[T]>Fires once per element added (push, unshift, splice insertions, index assignment, etc.)
elementRemovedEmitterTEmitter<[T]>Fires once per element removed
lengthPropertyNumberPropertyMirrors .length; read-only in practice — don't set it directly

Array-like and convenience methods

All standard Array<T> methods work as expected (push, pop, shift, unshift, splice, map, filter, forEach, indexing, for...of, ...). createObservableArray also layers on PhET-specific convenience methods:

MethodEffect
add( element )Alias for push( element )
addAll( elements )Pushes every element in elements
remove( element )Removes the first matching element
removeAll( elements )Removes every matching element in elements
clear()Empties the array (pops until empty, so removal notifications still fire)
count( predicate )Number of elements matching predicate
find( predicate, fromIndex? )First matching element, or undefined
get( index )Same as array[index]
getArrayCopy()A plain (non-observable) T[] snapshot
shuffle( random )Reorders in place via a { shuffle }-shaped random source, without firing add/remove notifications
addItemAddedListener / removeItemAddedListenerLegacy aliases for elementAddedEmitter.addListener / removeListener
addItemRemovedListener / removeItemRemovedListenerLegacy aliases for elementRemovedEmitter.addListener / removeListener
reset()Restores the array to the elements/length it was constructed with
dispose()Disposes elementAddedEmitter, elementRemovedEmitter, and lengthProperty

Options

ts
const readingsArray = createObservableArray<number>( {
  elements: [ 0, 0, 0 ]
} );
OptionEffect
elementsInitial contents (mutually exclusive with length)
lengthInitial length, filled with undefined (mutually exclusive with elements)
elementAddedEmitterOptionsOptions forwarded to the internal elementAddedEmitter
elementRemovedEmitterOptionsOptions forwarded to the internal elementRemovedEmitter
lengthPropertyOptionsOptions forwarded to the internal lengthProperty

It's a function, not a class

There is no ObservableArray constructor to new up — call createObservableArray<T>(options?) and you get back a value that satisfies both T[] and the observable API table above. The exported ObservableArray<T> name from scenerystack/axon is a type, useful for annotating a field or parameter, not a runtime class.

Prefer this over an array of Properties for "add/remove" collections

If you find yourself keeping a plain T[] alongside a manually-maintained Emitter for adds and another for removes, that's exactly what createObservableArray already does — and it keeps lengthProperty in sync for you, which is easy to get wrong by hand. See DerivedProperty if you need a value computed from the array's contents (e.g. a sum), by pairing DerivedProperty.deriveAny with lengthProperty or recomputeDerivation() triggered from elementAddedEmitter/elementRemovedEmitter.