Overview
Observables are the foundation of Legend-State. They are reactive containers that track their values and notify listeners when values change. Observables can wrap any JavaScript value: primitives, objects, arrays, Maps, Sets, and even Promises.
Creating Observables
Basic Observable
Create an observable using the observable() function:
import { observable } from '@legendapp/state'
// Primitive values
const count$ = observable ( 0 )
const name$ = observable ( 'Alice' )
const isActive$ = observable ( true )
// Objects
const user$ = observable ({
name: 'Alice' ,
age: 30 ,
email: '[email protected] '
})
// Arrays
const items$ = observable ([ 1 , 2 , 3 , 4 , 5 ])
// Nested structures
const state$ = observable ({
user: {
profile: {
name: 'Alice' ,
settings: {
theme: 'dark'
}
}
}
})
Observable with Undefined
Create an observable without an initial value:
const data$ = observable < string >()
console . log ( data$ . get ()) // undefined
data$ . set ( 'Hello' )
console . log ( data$ . get ()) // 'Hello'
Observable Primitive
For performance-critical scenarios with primitive values, use observablePrimitive():
import { observablePrimitive } from '@legendapp/state'
const count$ = observablePrimitive ( 0 )
const flag$ = observablePrimitive ( false )
observablePrimitive() is optimized for primitive values and doesn’t support nested properties.
Observable Types
Objects
When you create an observable from an object, each property becomes observable:
const user$ = observable ({
name: 'Alice' ,
age: 30
})
// Access nested observables
user$ . name . get () // 'Alice'
user$ . age . get () // 30
// Each property is also an observable
user$ . name . onChange (() => {
console . log ( 'Name changed!' )
})
Arrays
Arrays work like objects, with each index becoming an observable:
const todos$ = observable ([
{ id: 1 , text: 'Buy milk' , done: false },
{ id: 2 , text: 'Walk dog' , done: true }
])
// Access elements
todos$ [ 0 ]. text . get () // 'Buy milk'
todos$ [ 1 ]. done . get () // true
// Array methods are supported
todos$ . push ({ id: 3 , text: 'Write docs' , done: false })
const completed = todos$ . filter ( todo => todo . done . get ())
Maps and Sets
Maps and Sets are fully supported:
// Observable Map
const users$ = observable ( new Map ([
[ 'alice' , { name: 'Alice' , age: 30 }],
[ 'bob' , { name: 'Bob' , age: 25 }]
]))
users$ . get ( 'alice' ). name . get () // 'Alice'
users$ . set ( 'charlie' , { name: 'Charlie' , age: 35 })
// Observable Set
const tags$ = observable ( new Set ([ 'javascript' , 'react' ]))
tags$ . size // 2
tags$ . add ( 'typescript' )
tags$ . has ( 'react' ) // true
Booleans
Boolean observables have a special toggle() method:
const isOpen$ = observable ( false )
isOpen$ . toggle () // Sets to true
isOpen$ . toggle () // Sets to false
Type Signatures
function observable < T >() : Observable < T | undefined >
function observable < T >(
value : Promise < T > | (() => T ) | T
) : Observable < T >
function observablePrimitive < T >( value : Promise < T >) : ObservablePrimitive < T >
function observablePrimitive < T >( value ?: T ) : ObservablePrimitive < T >
Observable Interface
Every observable implements the following core methods:
interface Observable < T > {
get () : T // Get the current value (reactive)
peek () : T // Get the current value (non-reactive)
set ( value : T ) : void // Set a new value
onChange ( callback ) : void // Listen for changes
delete () : void // Delete the observable
}
The Observable type automatically infers the correct structure based on your data. Objects become ObservableObject, arrays become ObservableArray, etc.
Working with Promises
Observables can wrap Promises, which resolve asynchronously:
const data$ = observable (
fetch ( '/api/user' ). then ( res => res . json ())
)
// Initially undefined
console . log ( data$ . get ()) // undefined
// After promise resolves
await when ( data$ )
console . log ( data$ . get ()) // { name: 'Alice', ... }
// Check loading state
if ( data$ . get ()) {
console . log ( 'Data loaded!' )
}
Best Practices
Use $ Suffix Conventionally, append $ to observable variable names to distinguish them from regular values.
Avoid Over-nesting While deep nesting is supported, consider flattening your state structure for better performance.
Type Your Observables Always provide TypeScript types for better autocomplete and type safety.
Use Primitives Wisely Use observablePrimitive() for counters, flags, and other simple values that change frequently.
Next Steps
Getting and Setting Learn how to read and update observable values
Observing Changes Set up listeners to react to changes