Intercept & Observe {🚀}
⚠️ Warning: intercept and observe are low level utilities, and should not be needed in practice. Use some form of reaction instead, as observe
doesn't respect transactions and doesn't support deep observing of changes. Using these utilities is an anti-pattern. If you intend to get access to the old and new value using observe
, use reaction
instead. ⚠️
observe
and intercept
can be used to monitor the changes of a single observable, but they don't track nested observables.
intercept
can be used to detect and modify mutations before they are applied to the observable (validating, normalizing or cancelling).observe
allows you to intercept changes after they have been made.
Intercept
Usage: intercept(target, propertyName?, interceptor)
Please avoid this API. It basically provides a bit of aspect-oriented programming, creating flows that are really hard to debug. Instead, do things like data validation before updating any state, rather than during.
target
: the observable to guard.propertyName
: optional parameter to specify a specific property to intercept. Note thatintercept(user.name, interceptor)
is fundamentally different fromintercept(user, "name", interceptor)
. The first tries to add an interceptor to the currentvalue
insideuser.name
, which might not be an observable at all. The latter intercepts changes to thename
property ofuser
.interceptor
: callback that is invoked for each change that is made to the observable. Receives a single change object describing the mutation.
The intercept
should tell MobX what needs to happen with the current change.
Therefore it should do one of the following things:
- Return the received
change
object as-is from the function, in which case the mutation will be applied. - Modify the
change
object and return it, for example to normalize the data. Not all fields are modifiable, see below. - Return
null
, this indicates that the change can be ignored and shouldn't be applied. This is a powerful concept with which you can for example make your objects temporarily immutable. - Throw an exception, if for example some invariant isn't met.
The function returns a disposer
function that can be used to cancel the interceptor when invoked.
It is possible to register multiple interceptors to the same observable.
They will be chained in registration order.
If one of the interceptors returns null
or throws an exception, the other interceptors won't be evaluated anymore.
It is also possible to register an interceptor both on a parent object and on an individual property.
In that case the parent object interceptors are run before the property interceptors.
const theme = observable({
backgroundColor: "#ffffff"
})
const disposer = intercept(theme, "backgroundColor", change => {
if (!change.newValue) {
// Ignore attempts to unset the background color.
return null
}
if (change.newValue.length === 6) {
// Correct missing '#' prefix.
change.newValue = "#" + change.newValue
return change
}
if (change.newValue.length === 7) {
// This must be a properly formatted color code!
return change
}
if (change.newValue.length > 10) {
// Stop intercepting future changes.
disposer()
}
throw new Error("This doesn't look like a color at all: " + change.newValue)
})
Observe
Usage: observe(target, propertyName?, listener, invokeImmediately?)
See above notice, please avoid this API and use reaction
instead.
target
: the observable to observe.propertyName
: optional parameter to specify a specific property to observe. Note thatobserve(user.name, listener)
is fundamentally different fromobserve(user, "name", listener)
. The first observes the currentvalue
insideuser.name
, which might not be an observable at all. The latter observes thename
property ofuser
.listener
: callback that will be invoked for each change that is made to the observable. Receives a single change object describing the mutation, except for boxed observables, which will invoke thelistener
with two parameters:newValue, oldValue
.invokeImmediately
: false by default. Set it to true if you wantobserve
to invoke thelistener
directly with the state of the observable, instead of waiting for the first change. Not supported (yet) by all kinds of observables.
The function returns a disposer
function that can be used to cancel the observer.
Note that transaction
does not affect the working of the observe
method(s).
This means that even inside a transaction observe
will fire its listeners for each mutation.
Hence autorun
is usually a more powerful and declarative alternative to observe
.
observe
reacts to mutations when they are being made, while reactions like autorun
or reaction
react to new values when they become available. In many cases the latter is sufficient.
Example:
import { observable, observe } from "mobx"
const person = observable({
firstName: "Maarten",
lastName: "Luther"
})
// Observe all fields.
const disposer = observe(person, change => {
console.log(change.type, change.name, "from", change.oldValue, "to", change.object[change.name])
})
person.firstName = "Martin"
// Prints: 'update firstName from Maarten to Martin'
// Ignore any future updates.
disposer()
// Observe a single field.
const disposer2 = observe(person, "lastName", change => {
console.log("LastName changed to ", change.newValue)
})
Related blog: Object.observe is dead. Long live mobx.observe
Event overview
The callbacks of intercept
and observe
will receive an event object which has at least the following properties:
object
: the observable triggering the event.debugObjectName
: the name of the observable triggering the event (for debugging).observableKind
: the type of the observable (value, set, array, object, map, computed).type
(string): the type of the current event.
These are the additional fields that are available per type:
Observable type | Event type | Property | Description | Available during intercept | Can be modified by intercept |
---|---|---|---|---|---|
Object | add | name | Name of the property being added. | √ | |
newValue | The new value being assigned. | √ | √ | ||
update* | name | Name of the property being updated. | √ | ||
newValue | The new value being assigned. | √ | √ | ||
oldValue | The value that is replaced. | ||||
Array | splice | index | Starting index of the splice. Splices are also fired by push , unshift , replace , etc. | √ | |
removedCount | Amount of items being removed. | √ | √ | ||
added | Array with items being added. | √ | √ | ||
removed | Array with items that were removed. | ||||
addedCount | Amount of items that were added. | ||||
update | index | Index of the single entry being updated. | √ | ||
newValue | The newValue that is / will be assigned. | √ | √ | ||
oldValue | The old value that was replaced. | ||||
Map | add | name | The name of the entry that was added. | √ | |
newValue | The new value that is being assigned. | √ | √ | ||
update | name | The name of the entry being updated. | √ | ||
newValue | The new value that is being assigned. | √ | √ | ||
oldValue | The value that has been replaced. | ||||
delete | name | The name of the entry being removed. | √ | ||
oldValue | The value of the entry that was removed. | ||||
Boxed & computed observables | create | newValue | The value that was assigned during creation. Only available as spy event for boxed observables. | ||
update | newValue | The new value being assigned. | √ | √ | |
oldValue | The previous value of the observable. |
Note: object update
events won't fire for updated computed values (as those aren't mutations). But it is possible to observe them by explicitly subscribing to the specific property using observe(object, 'computedPropertyName', listener)
.