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 false to not track or shallow to track only when keys are added/removed. See Tracking for more details.


peek() is the same as get(false), returning the value without tracking it.


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. But, primitives (string, number, boolean) have a value that you can modify directly.

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

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

obs.text.value = 'hi'
// ✅ Setting value works

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

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

// ✅ Calling set on a primitive works.

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, () => {
    // 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' } } }