0.16 to 0.17

Primitives are now returned as observables

Observables previously tried to be clever by returning primitives directly, which was great in making it easy to work with state directly. But especially as the goal has moved more towards fine-grained reactivity, the balance shifted towards observable objects being better. So accessing primitives through state now returns observables like anything else.

Raw primitives:

  • Pro: Easy to work with
  • Con: Required obs() to get the observable to pass to props or render directly
  • Con: Easy to track a value without realizing it

Observable primitives

  • Pro: More consistent
  • Pro: Easier to deal with undefined
  • Pro: Can dot through undefined paths easily
  • Pro: Doesn’t need obs() or set by key
  • Pro: Easier to use fine-grained features without obs() everywhere
  • Pro: Easier to pass as props without needing obs()
  • Con: Requires get() or .value for primitives

Changes to make:


Wherever you were accessing primitives directly, add a .get() to the end of it.

set(key, value)

Change set by key to access the node first. It will now work fine if the node is undefined.

From: state.profile.set('name', 'Annyong')

To:     state.profile.name.set('Annyong')


Just remove it. The default behavior is now the same as what obs() did before.

Hooks renamed

useComputed is now useSelector, re-rendering only when the return value changes.

useComputed now returns a computed observable.

0.15 to 0.16

enableLegendStateReact() to observe, removed observer

Legend-State now automatically tracks observable access in any component. To set it up, just call enableLegendStateReact() at the beginning of your app.

Now observer is no longer needed, so just remove all usage of observer.

0.14 to 0.15


There are now three levels of safety: Unsafe, Default, and Safe. Default is new and allows direct assignment to primitives but prevents directly assigning to everything else. The previous default behavior was Unsafe so you may see errors if you were directly assigning to objects/arrays/etc... TypeScript should show errors so it should be easy to find them. Replace those with .set(...) or pass in false as the second parameter to observable to go back to "Unsafe" mode.

// 1. Unsafe: Use false for the previous unsafe behavior
const obs = observable({ ... }, /*safe*/ false)

// 2. Default: The new default behavior prevent directly assigning to objects, but allows directly assining to primitives
const obs = observable({ text: 'hello',  obj: {} })

obs.text = 'hi'
// ✅ Setting a primitive works in default mode but not in safe mode.

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

// 3. Safe: Safe mode prevents all direct assignment
const obs = observable({ text: 'hello',  obj: {} }, /*safe*/true)

obs.text = 'hi'
// ❌ Error. Cannot assign directly in safe mode.

Renamed ref to obs

ref was a bit unclear and conflicted with React - the new feature to directly render observables requires a ref property. So it is now renamed to obs, which feels more intuitive as it is used to get an observable.

const state = observable({ text: '' })

// Before
const textRef = state.ref('text')
const textRef2 = state.text.ref()

// Now
const textObs = obs.obs('text')
const textObs2 = obs.text.obs()

Array optimizations

The array optimizations are now opt-in, because they are only useful in React and can potentially have some unexpected behavior in React if modifying the DOM externally. You can enable them by using the For component with the optimized prop. See Arrays for more.

const obs = observable({ items: [] })

const Row = observer(function Row({ item }) {
    return <div>{item.text}</div>

const List = observer(function () {
    // The optimized prop enables the optimizations which were previously default
    return <For each={list} item={Row} optimized />


Since there's now a additionally the optimized tracking for arrays, the shallow option on get() and obs() now has another option. So instead of passing shallow to an observable, use the Tracking namespace now.

import { Tracking } from '@legendapp/state'

const obs = observable([])

// Before

// Now


The observableBatcher namespace is removed and the batching functions are now exported on their own.

import { batch, beginBatch, endBatch } from '@legendapp/state'

// begin/end

// batch()
batch(() => {

Change functions => observe/when

The new observe and when functions can automatically track all observables accessed while running them. This made the old extra change utilities unnecessary, so onTrue, onHasValue, onEquals, and onChangeShallow have been removed, saving 200 bytes (7%) from the bundle size. These are the new equivalents:

import { observe, when } from "@legendapp/state"

const obs = observable({ value: undefined })

// onTrue
// New onTrue equivalent
when(() => obs.value === true, handler)

// onHasValue
obs.value.onHasValue('text', handler)
// onHasValue equivalent
when(() => obs.value, handler)

// onEquals
obs.value.onEquals('text', handler)
// onEquals equivalent
when(() => obs.value === 'text', handler)

// onChangeShallow
// onChangeShallow equivalent
obs.value.onChange(handler, { shallow: true })

Primitive current => value

Primitive observables are now wrapped in { value } instead of { current }. You can also now modify the value directly.

const obs = observable(10)
// Before
obs.current === 10
obs.curent = 20 // ❌ Error
// Now
obs.value === 10
obs.value = 20 // ✅ Works

Renamed observableComputed and observableEvent

observableComputed is now just computed and observableEvent is now just event.

import { computed, event } from '@legendapp/state'

// Before
const value = observableComputed(() => ...)
// Now
const value = computed(() => ...)

// Before
const evt = observableEvent(() => ...)
// Now
const evt = event(() => ...)

Renamed LS to Bindable

The automatically bound exports are now named better and in their own exports, so change your exports from LS to:

// Web
import { Bindable } from '@legendapp/state/react-components'

// React Native
import { Bindable } from '@legendapp/state/react-native-components'

Renamed Isolate to Computed

The control flow component Isolate is renamed to Computed for naming consistency.

Removed memo and isolate props

We found these confusing in practice as it wasn't super clear when a component was getting memoized, and it's not much extra work to use the Memo and Computed components directly. If you were using those, switch to the Computed and Memo components instead

// Before
<div memo>...</div>
<div computed>...</div>

// Now