Reactivity
Listening for changes is the core purpose of observables, so Legend-State provides many options. Critically, you can listen to changes at any level in an object's hierarchy and it will be notified by changes in any children.
onChange
onChange
listens to an observable for any changes anywhere within it. Use this as specifically as possible because it will fire notifications for every change recursively up the tree.
const obs = observable({ text: 'hi' })
obs.text.onChange(({ value }) => console.log('text changed to', value))
obs.onChange(({ value }) => console.log('obs changed to', value))
obs.text.set('hello')
// Log: text changed to "hello"
// Log: obs changed to { text: "hello" }
onChange
has some extra options for more advanced use:
getPrevious
: Function to compare with the previous value. It is a function to let you opt into getting the previous value if needed, because it has some performance cost in cloning the object to compute the previous value.changes
: Array of all of the changes to this observable in the latest batch. This is intended mainly for internal usage by the persistence plugins to know what to sync/update and the history plugin to track all changes, but it may be good for other uses too.trackingType
: Whether to track only shallow changesinitial
: Whether to run the callback immediately with the current valueimmediate
: Whether to run the callback immediately instead of within a batch. This is used internally bycomputed
to make sure its value is always correct, but it may be useful for other specific uses.
// Full example
obs.onChange(({ value, getPrevious, changes }) => {
const prev = getPrevious();
changes.forEach(({ path, valueAtPath, prevAtPath }) => {
console.log(valueAtPath, 'changed at', path, 'from', prevAtPath)
})
}, { initial: true, trackingType: true })
Dispose of listeners
Listening to an observable returns a dispose function to stop listening. Just call it when you want to stop listening.
const obs = observable({ text: 'hello' })
const onChange = () => { ... }
const dispose = obs.text.onChange(onChange)
// Cancel listening manually
dispose()
observe
observe
can run arbitrary code when observables change, and automatically tracks the observables accessed while running, so it will update whenever any accessed observable changes.
This can be useful to use multiple observables at once, for the benefit of cleanup effects, or if you just like it more than onChange 😎.
The callback parameter has some useful properties:
num
: How many times it's run. Use this to do something only the first time or not the first time.previous
: The previous value, which will be undefined on the first run and set to the return valuecancel
: Set totrue
to stop tracking the observables when you are done observingonCleanup
: A function to call before running the selector again
observe
has an optional second reaction
parameter which will run after the selector, and does not track changes. This can be useful for observing an event
or a single observable
.
import { observe, observable } from "@legendapp/state"
const state = observable({ isOnline: false, toasts: [] })
const dispose = observe((e) => {
// This observe will automatically track state.isOnline for changes
if (!state.isOnline.get()) {
// Show an "Offline" toast when offline
const toast = { id: 'offline', text: 'Offline', color: 'red' }
state.toasts.push(toast)
// Remove the toast when the observe is re-run, which will be when isOnline becomes true
e.onCleanup = () => state.toasts.splice(state.toasts.indexOf(toast), 1)
}
})
// Cancel the observe
dispose()
Or use the second parameter to run a reaction when a selector changes. It has an additional value
parameter, which contains the value of the selector.
// Observe the return value of a selector and observe all accessed observables
observe(state.isOnline, (e) => {
console.log('Online status', e.value)
})
// Observe the return value of a selector and observe all accessed observables
observe(() => state.isOnline.get() && state.user.get(), (e) => {
console.log('Signed in status', e.value)
})
when
when
runs the given function only once when the predicate returns a truthy value, and automatically tracks the observables accessed while running the predicate so it will update whenever one of them changes. When the value becomes truthy it will call the function and dispose the listeners. If not given a function it will return a promise that resolves when the predicate returns a truthy value.
The predicate can either be an observable or a function.
import { when } from "@legendapp/state"
const obs = observable({ ok: false })
// Option 1: Promise
await when(obs.ok)
// Option 2: callback
const dispose = when(() => obs.ok.get(), () => console.log("Don't worry, it's ok"))
// Cancel listening manually
dispose()
whenReady
whenReady
is the same as when
except it waits for objects and arrays to not be empty.
import { whenReady } from "@legendapp/state"
const obs = observable({ arr: [] })
whenReady(() => obs.arr.get(), () => console.log("Array has some values"))
// Not ready yet
obs.arr.push('hello')
// "Array has some values"
computed
computed
automatically tracks the observables accessed while computing, so you can return a computed value based on multiple observables, and it will update whenever one of them changes.
computed
is lazy so it won't run the compute function until you get()
the value the first time.
import { computed } from "@legendapp/state"
const obs = observable({ test: 10, test2: 20 })
const computed = computed(() => obs.test.get() + obs.test2.get())
// computed.get() === 30
obs.test.set(5)
// computed.get() === 25
Two-way computed
computed
has an optional set
parameter to run when setting the value. This lets you pass state changes onto the target observables, so the computed is bound to the targets in both directions. Without a set
parameter, a one-way computed is read-only.
const selected = observable([false, false, false])
const selectedAll = computed(
// selectedAll is true when every element is selected
() => selected.every(obs => obs.get()),
// setting selectedAll sets the value of every element
(value) => selected.forEach(obs => obs.set(value))
)
selectedAll.set(true);
// selected.get() === [true, true, true]
event
event
works like an observable without a value. You can listen for changes as usual, and dispatch it manually whenever you want. This can be useful for simple events with no value, like onClosed.
import { event } from "@legendapp/state"
const onClosed = event()
// Simply pass a callback to the `on` function
onClosed.on(() => { ... })
// Or use it with 'onChange' like other observables
onClosed.onChange(() => { ... })
// Dispatch the event to call listeners
onClosed.fire()