Observables
observable(value)
@observable classProperty = value
Observables can be JS primitives, references, plain objects, class instances, arrays and maps. observable(value)
is a convenience overload, that always tries to create the best matching observable types. You can also directly create the desired observable type.
The following conversion rules are applied, but can be fine-tuned by using modifiers.
- If value is an instance of an ES6 map: a new Observable Map will be returned. Observable maps are very useful if you don't want to react to just the change of a specific entry, but also to the addition or removal of entries.
- If value is an array, a new Observable Array will be returned.
- If value is an object without a prototype or its prototype is
Object.prototype
, the object will be cloned and all its current properties will be made observable. (Observable Object). - If value is an object with a prototype, a JavaScript primitive or function, a Boxed Observable will be returned. MobX will not make objects with a prototype automatically observable; as that is the responsibility of its constructor function. Use
extendObservable
in the constructor, or@observable
in its class definition instead.
These rules might seem complicated at first sight, but you will notice that in practice they are very intuitive to work with.
- To create dynamically keyed objects always use maps! Only initially existing properties on an object will be made observable, although new ones can be added using
extendObservable
. - To use the
@observable
decorator, make sure that decorators are enabled in your transpiler. - By default, making a data structure observable is infective; that means that
observable
is applied automatically to any value that is contained by the data structure, or will be contained by the data structure in the future. This behavior can be changed by using modifiers or shallow.
Some examples:
const map = observable.map({ key: "value"});
map.set("key", "new value");
const list = observable([1, 2, 4]);
list[2] = 3;
const person = observable({
firstName: "Clive Staples",
lastName: "Lewis"
});
person.firstName = "C.S.";
const temperature = observable(20);
temperature.set(25);
@observable
Decorator that can be used on ES7 - or TypeScript class properties to make them observable. The @observable
can be used on instance fields and property getters. This offers fine-grained control on which parts of your object become observable.
import { observable, computed } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
}
If your environment doesn't support decorators or field initializers, @observable key = value;
is sugar for extendObservable(this, { key: value })
.
Note:
All the properties are being defined lazily as soon as any of them is accessed. Before that they are only defined on the class prototype.
In other words,
const line = new OrderLine(); console.log("price" in line); // true console.log(line.hasOwnProperty("price")); // false, the price _property_ is defined on the class, although the value will be stored per instance. line.amount = 2; console.log(line.hasOwnProperty("price")); // true, now all the properties are defined on the instance
The @observable
decorator can be combined with modifiers like asStructure
:
@observable position = asStructure({ x: 0, y: 0})
Enabling Decorators in Your Transpiler
Decorators are not supported by default when using TypeScript or Babel pending a definitive definition in the ES standard.
- For typescript, enable the
--experimentalDecorators
compiler flag or set the compiler optionexperimentalDecorators
totrue
intsconfig.json
(Recommended)
API
Name | Description |
---|---|
@observable property = value |
observable can be used as a property decorator |
observable.box(value) & observable.shallowBox(value) |
Creates an observable box that stores an observable reference to a value. Use get() to get the current value of the box, and set() to update it. This is the foundation on which all other observables are built, but in practice you will use it rarely. Normal boxes will automatically try to turn any new value into an observable if it isn't already. Use shallowBox to disable this behavior. |
observable.object(value) & observable.shallowObject(value) |
Creates a clone of the provided object and makes all its properties observable. By default, any values in those properties will be made observable as well, but when using shallowObject only the properties will be made into observable references, but the values will be untouched. (This holds also for any values assigned in the future.) |
observable.array(value) & observable.shallowArray(value) |
Creates a new observable array based on the provided value. Use shallowArray if the values in the array should be turned into observables. |
observable.map(value) & observable.shallowMap(value) |
Creates a new observable map based on the provided value. Use shallowMap if the values in the map should be turned into observables. Use map whenever you want to create a dynamically keyed collection and the addition / removal of keys needs to be observed. Note that only string keys are supported. |
extendObservable & extendShallowObservable |
For each key/value pair in each propertyMap , a (new) observable property will be introduced on the target object. This can be used in constructor functions to introduce observable properties without using decorators. If a value of the propertyMap is a getter function, a computed property will be introduced. |
extendObservable
Quite similar to Object.assign
, extendObservable
takes two or more arguments, a target
object, and one or more properties
maps. It adds all key-value pairs from the properties to the target
as observable properties and returns the target
object.
var Person = function(firstName, lastName) {
// initialize observable properties on a new instance
extendObservable(this, {
firstName: firstName,
lastName: lastName
});
}
var matthew = new Person("Matthew", "Henry");
// add a observable property to an already observable object
extendObservable(matthew, {
age: 353
});
Note:
observable.object(object)
is actually an alias forextendObservable({}, object)
.Note:
The property maps are not always copied literally onto the target, but they are considered property descriptor. Most values are copied as-is, but values wrapped in a modifier as treated specially. And so are properties that have a getter.
extendShallowObservable
extendShallowObservable
is like extendObservable
, except that by default the properties will not automatically convert their values into observables. So it is similar to calling extendObservable
with observable.ref
modifier for each property.
Note:
observable.deep
can be used to get the automatic conversion back for a specific property.
Modifiers
Modifiers can be used to define special behavior for certain properties. For example, observable.ref
creates an observable reference which doesn't automatically convert its values into observables, and computed
introduces a derived property:
var Person = function(firstName, lastName) {
// initialize observable properties on a new instance
extendObservable(this, {
firstName: observable.ref(firstName),
lastName: observable.ref(lastName),
fullName: computed(function() {
return this.firstName + " " + this.lastName
})
});
}
Modifiers can be used as decorators or in combination with extendObservable
and observable.Object
to change the autoconversion rules for specific properties.
The following modifiers are available:
observable.deep
: This is the default modifier, used by any observable. It converts any assigned, non-primitive value into an observable if it isn't one yet.observable.ref
: Disables automatic observable conversion, just creates an observable reference instead.observable.shallow
: Can only be used in combination with collections. Turns any assigned collection into a collection, which is shallowly observable (instead of deep). In other words, the values inside the collection won't become observables automatically.computed
: Creates a derived property.action
: Creates an action.
Modifiers can be used as decorators:
class TaskStore {
@observable.shallow tasks = []
}
Or as property modifiers in combination with observable.object
/ observable.extendObservable
. Note that modifiers always 'stick' to the property. So they will remain in effect even if a new value is assigned.
const taskStore = observable({
tasks: observable.shallow([])
})
Deep Observability
When MobX creates an observable object (using observable
, observable.object
, or extendObservable
), it introduces observable properties which by default use the deep
modifier. The deep modifier basically recursively calls observable(newValue)
for any newly assigned value. Which in turn uses the deep
modifier... you get the idea.
This is a very convenient default. Without any additional effort all values assigned to an observable will themselves be made observable too (unless they already are), so no additional effort is required to make objects deep observable.
Reference Observability
In some cases, however, objects don't need to be converted into observables. Typical cases are immutable objects, or objects that are not managed by you but by an external library. Examples are JSX elements, DOM elements, native objects like History, window, or etc. To those kind of objects, you just want to store a reference without turning them into an observable.
For these situations there is the ref
modifier. It makes sure that an observable property is created, which only tracks the reference but doesn't try to convert its value. For example:
class Message {
@observable message = "Hello world"
// fictional example, if author is immutable, we just need to store a reference and shouldn't turn it into a mutable, observable object
@observable.ref author = null
}
function Message() {
extendObservable({
message: "Hello world",
author: observable.ref(null)
})
}
Note:
An observable, boxed reference can be created by using
const box = observable.shallowBox(value)
.
Shallow Observability
The observable.shallow
modifier applies observability 'one-level-deep'. You need those if you want to create a collection of observable references. If a new collection is assigned to a property with this modifier, it will be made observable, but its values will be left as is, so unlike deep
, it won't recurse.
class AuthorStore {
@observable.shallow authors = []
}
In the above example, an assignment of a plain array with authors to the authors
will update the authors with an observable array, containing the original, non-observable authors.
Note:
The following methods can be used to create shallow collections manually:
observable.shallowObject
observable.shallowArray
observable.shallowMap
extendShallowObservable
Action & Computed
The following can be used as modifiers as well:
action
action.bound
computed
computed.struct
const taskStore = observable({
tasks: observable.shallow([]),
taskCount: computed(function() {
return this.tasks.length
}),
clearTasks: action.bound(function() {
this.tasks.clear()
})
})
Effect of Modifiers
class Store {
@observable/*.deep*/ collection1 = []
@observable.ref collection2 = []
@observable.shallow collection3 = []
}
const todos = [{ test: "value" }]
const store = new Store()
store.collection1 = todos;
store.collection2 = todos;
store.collection3 = todos;
After these assignments:
collection1 === todos
is FALSE. The contents oftodos
will be cloned into a new observable array.collection1[0] === todos[0]
is FALSE. The firsttodo
was a plain object and hence it was cloned into an observable object which is stored in the array.collection2 === todos
is TRUE. Thetodos
are kept as is, and are non-observable. Only thecollection2
property itself is observable.collections2[0] === todos[0]
is TRUE because of the previous point.collection3 === todos
is FALSE.collection3
is a new observable array.collection3[0] === todos[0]
is TRUE. The value ofcollection3
was only shallowly turned into an observable, but the contents of the array is left as is.