Observable
You can put anything in an observable: primitives, deeply nested objects, arrays, functions, etc… Observables work just like normal objects so you can interact with them without any extra complication. Just call get()
to get a value and set(...)
to modify it.
Observables do not modify the underlying data all. They use Proxy to expose observable functions and track changes, so an observable is a Proxy pointing to the actual data.
Observable methods
get()
You can use get()
to get the actual value of any observable.
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.
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 observing contexts for more details.
peek()
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.
set()
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.
Note that set
sets the given value into the raw data without modifying it. Legend-State does deep equality checking to notify of changes to each property, so setting with a clone of an object will not notify of any changes because all properties are the same.
assign()
Assign is a shallow operation matching Object.assign
to set multiple properties at once. If you want a deep merge, see mergeIntoObservable. These batch all individual set operations so that observers only update once.
delete()
Observables provide a delete
function to delete a key from an object.
delete
works on array elements as well, removing the element from the array.
Computed Observables
Functions
Observables can have functions anywhere within them. You can use these for whatever you want, such as adding extra behavior when setting.
Note that observing contexts track all observable get()
calls, including within any called functions. So if a function called from within an observer calls get()
that will be tracked too.
Computed Functions
Any function in an observable can be used a computed observable, whether at the root or in any child. Computed functions are lazy: a function is turned into an observable when you first call get()
or peek()
on it. It will then re-compute itself whenever the observables it accesses with get()
are changed.
The difference between using it as a function vs. as a computed observable is that a computed observable is an object that caches the value.
fullName()
is a function that re-computes whenever you call it.fullName.get()
creates a computed observable that re-computes itself whenever its dependencies change.
Async Observables
Creating an observable with a Promise or async function will initialize it to undefined
, and it will be updated with the value of the Promise when it resolves.
Asynchronous observables can be paired with when to activate the function and resolve when the observable’s Promise is resolved.
You can access the status of an async observable with the syncState helper, which is an observable itself. The most common usage is to check its loaded or error states:
Linked observables
Two-Way Linked
linked
creates an observable bound to both get
and set
functions. This lets you bind or transform a single or multiple other observable values. For example it could be used to create a “Select All” checkbox.
Or it could be used to automatically deserialize/serialize a string value.
Initial value
When creating an asynchronous observable with a Promise you may want it to have an initial default value until the promise resolves. You can use the initial
property of linked
to do that.
Advanced Computeds
Link to another observable
If you return an observable from a computed function, it will create a two-way link to the target observable. Interaction with the linked observable will then pass through to the target.
Observing contexts tracking the linking observable will re-run both when the linked observable’s value changes and when the link itself changes.
In this example, the observable that selectedItem
points to is changed by setting selectedIndex
. And because it’s a direct link to the target observable, set
operations will pass through to the target observable.
This could also be used to transform objects to another shape while still linking to the original value. So for example you could filter the values of an object into an array, with each element in the array pointing to the original observable.
Lookup table
A function with a single string
key can be used as a lookup table (an object with a string key). Accessing it by index will call the function to create a computed observable by that key.
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.
Notes
Safety
Modifying an observable can have a large effect such as re-rendering or syncing with a database, so it uses a purposeful set
rather than simple assignments. This prevents potentially catastrophic mistakes and looks visually different than a variable assignment so that it is clear what is happening.
If you really want to assign directly to observables, there is an extension to add $
as a property you can get/set. See configuration for details.
undefined
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.
Or you could set a value inside an undefined object, and it will fill out the object tree to make it work.
Arrays
Observable arrays have all of the normal array functions as you’d expect, but some are modified for observables.
All looping functions set up shallow tracking automatically, as well as provide the observable in the callback. This includes:
- every
- filter
- find
- findIndex
- forEach
- includes
- join
- map
- some
Additionally, filter
returns an array of observables and find
returns an observable (or undefined).
If you don’t want this extra observable behavior, get()
or peek()
the observable to get the raw array to act on.
Observables are mutable
Legend-State does not use immutability because immutability is slow. It needs to do deep equality checking of changes to know which nodes to notify anyway, so immutability just isn’t needed. So there are two things to be careful of.
1. Modifying raw data breaks notifying of changes.
Observables are just wrappers around the underlying data, so if you modify the raw data you’re actually modifying the observable data without notifying of changes. Then if you set it back onto the observable, that just sets it to itself so nothing happens.
2. Don’t need to clone
A common pattern in React is to set state with a clone of the previous value, which is required because of immutability constraints in React. Legend-State does not have that constraint and cloning is bad for performance, so it’s better to do operations directly on the observables.