You can put any kind of data in observables. Observables don't change the raw data at all, and you can access the raw value at any point with get().

import { observable } from "@legendapp/state"

const obs = observable({ text: 'hello' })

// { text: 'hello' }

console.log(obs.text.get(), obs.text.get() === 'hello')
// 'hello', true


Observables use Proxy to expose observable functions and track changes, so an observable is a Proxy pointing to the actual data. You can use get() to get the actual value of any observable.

const obs = observable({ profile: { name: '' } })
const profile = { name: 'Test user' }

obs.profile.get()             // { name: 'Test user' }
obs.profile === profile       // ❌ false. The observable is not strictly equal to profile.
obs.profile.get() === profile // ✅ true. The raw data is exactly what was set.

Accessing properties through the observable will create a Proxy for every property accessed, but it will not do that while accessing the raw data. So you may want to retrieve the raw data before doing expensive computations that do not need to notify.

const obs = observable({ data: someHugeThing })
const { data } = obs.get()

// Nothing special happens when working with the raw data

Calling get() within a tracking context tracks the observable automatically. You can change that behavior with a parameter true to track only when keys are added/removed. See Tracking for more details.

obs.get(true) // Create a shallow listener


peek() returns the raw value in the same way as get(), but it does not automatically track it. Use this when you don't want the component/observing context to update when the value changes.


You can use set() to modify the observable, at any path within it. You can even set() on a node that is currently undefined, and it will fill in the object tree to make it work.

const obs = observable({ text: 'hi' })

// Set directly
obs.text.set('hello there')

// Set with a function relative to previous value
obs.set('text', (prev) => prev + ' there')

// Set will automatically fill out objects that were undefined


Assign is a shallow operation matching Object.assign. If you want a deep merge, see mergeIntoObservable.

const obs = observable({ text: 'hi' })

// Assign
obs.assign({ text: 'hi2' })


Observables provide a delete function to delete a key from an object.

const obs = observable({ text: 'hi' })

// Delete text


Observables are safe so that you cannot directly assign to them, which prevents accidentally overwriting state or accidentaly assigning huge objects into an observable.

const obs = observable({ text: 'hello', num: 10, obj: {} }, /*safe*/ true)

obs.text = 'hi'
// ❌ Can't set directly

// ✅ Calling set on a primitive works.

obs = {}
// ❌ Error. This would delete the observable.

obs.obj = {}
// ❌ Error. Cannot assign to objects directly directly.

obs.set({ text: 'hi', num: 20 })
// ✅ Calling set on an object works.

obs.assign({ text: 'hello there' })
// ✅ Calling assign on an object works.

obs.text.assign({ value: 'hello there' })
// ❌ Error. Cannot call assign on a primitive.


Because observables track nodes by path and not the underlying data, an observable points to a path within an object regardless of its actual value. So it is perfectly fine to access observables when they are currently undefined in the object.

You could to do this to set up a listener to a field whenever it becomes available.

const state = observable({ user: undefined })

when(state.user.uid, (uid) => {
    // Handle login

Or you could set a value inside an undefined object, and it fill out the object tree to make it work.

const state = observable({ user: undefined })


// state == { user: { profile: { name: 'Annyong' } } }