Computed Values
computed(() => expression
computed(() => expression, (newValue) => void)
computed(() => expression, options)
@computed get classProperty() { return expression; }
@computed.struct get classProperty() { return expression; }
@computed.equals(comparisonMethod) get classProperty() { return expression; }
Creates a computed property. The expression
should not have side effects but return a value. The expression will automatically be re-evaluated if any observables it uses changes, but only if it is in used by some reaction.
Comparison methods can be used to override the default detection on when something is changed and should be of value (value, value) => boolean
. Built-in comparisons are: comparer.identity
, comparer.default
, comparer.structural
.
(@)computed
Computed values are values that can be derived from the existing state or other computed values. Conceptually, they are very similar to formulas in spreadsheets. Computed values can't be underestimated, as they help you make your actual modifiable state as small as possible. Besides that they are highly optimized, so use them whenever possible.
Don't confuse computed
with autorun
. They are both reactively invoked expressions, but use @computed
if you want to reactively produce a value that can be used by other observers and autorun
if you don't want to produce a new value but rather want to achieve an effect. For example, imperative side effects like logging, making network requests, etc.
Computed values are automatically derived from your state if any value that affects them changes. Computed values can be optimized away in many cases by MobX as they are assumed to be pure. For example, a computed property won't re-run if it is not in use by some other computed property or reaction. In such cases, it will be suspended.
This automatic suspension if very convenient. If a computed value is no longer observed, for example the UI in which it was used no longer exists, MobX can automatically garbage collect it. This differs from autorun
's values where you must dispose of them yourself. It sometimes confuses people new to MobX, that if you create a computed property, but don't use it anywhere in a reaction, it will not cache its value and recompute more often than seems necessary. However, in real life situations this is by far the best default, and you can always forcefully keep a computed value awake if you need to, by using either observe
or keepAlive
.
Note:
Computed
properties are not enumerable. Nor can they be overwritten in an inheritance chain.
@computed
If you have decorators enabled, you can use the @computed
decorator on any getter of a class property to declaratively create computed properties.
import {observable, computed} from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
constructor(price) {
this.price = price;
}
@computed get total() {
return this.price * this.amount;
}
}
computed
Modifier
If your environment doesn't support decorators, use the computed(expression)
modifier in combination with extendObservable
/ observable
to introduce new computed properties.
@computed get propertyName(){ }
is basically sugar for extendObservable(this, { propertyName: get func() { } })
in the constructor call.
import {extendObservable, computed} from "mobx";
class OrderLine {
constructor(price) {
extendObservable(this, {
price: price,
amount: 1,
// valid:
get total() {
return this.price * this.amount
},
// also valid:
total: computed(function() {
return this.price * this.amount
})
})
}
}
Setters for Computed Values
Computed properties can also be written by using a getter function. Optionally, accompanied with a setter. Note: These setters cannot be used to alter the value of the computed property directly, but they can be used as 'inverse' of the derivation. For example,
const box = observable({
length: 2,
get squared() {
return this.length * this.length;
},
set squared(value) {
this.length = Math.sqrt(value);
}
});
And similarly,
class Foo {
@observable length = 2;
@computed get squared() {
return this.length * this.length;
}
set squared(value) { //this is automatically an action, no annotation necessary
this.length = Math.sqrt(value);
}
}
Note:
Always define the setter after the getters. Some TypeScript versions are known to declare two properties with the same name otherwise.
Note:
Setters require MobX 2.5.1 or higher.
var Person = function(firstName, lastName) {
// initialize observable properties on a new instance
extendObservable(this, {
firstName: firstName,
lastName: lastName,
get fullName() {
return this.firstName + " " + this.lastName
},
set fullName(newValue) {
var parts = newValue.split(" ")
this.firstName = parts[0]
this.lastName = parts[1]
}
});
}
computed(expression)
as Function
computed
can be invoked directly as function. Just like observable.box(primitive value)
creates a stand-alone observable. Use .get()
on the returned object to get the current value of the computation, or .observe(callback)
to observe its changes. This form of computed
is not used very often, but in some cases where you need to pass a "boxed" computed value around it might prove useful.
Example:
import {observable, computed} from "mobx";
var name = observable("John");
var upperCaseName = computed(() =>
name.get().toUpperCase()
);
var disposer = upperCaseName.observe(change => console.log(change.newValue));
name.set("Dave");
// prints: 'DAVE'
Options for computed
When using computed
as modifier or as box, it accepts a second options argument with the following optional arguments:
name
: (string) the debug name used in spy and the MobX devtoolscontext
: thethis
that should be used in the provided expressionsetter
: the setter function to be used. Without setter it is not possible to assign new values to a computed value. If the second argument passed tocomputed
is a function, this is assumed to be a setter.compareStructural
: By defaultfalse
. Whentrue
, the output of the expression is structurally compared with the previous value before any observer is notified about a change. This makes sure that observers of the computation don't re-evaluate if new structures are returned that are structurally equal to the original ones. This is very useful when working with point, vector, or color structures, for example. The same behaviour can be achieved by specifying theequals
option withcomparer.structural
.equals
: By defaultcomparer.default
. This acts as a comparison function for comparing the previous value with the next value. If this function considers the previous and next values to be equal, then observers will not be re-evaluated. This is useful when working with structural data, and types from other libraries. For example, a computed moment instance could use(a, b) => a.isSame(b)
. If specified, this will overridecompareStructural
.
@computed.struct
for Structural Comparison
The @computed
decorator does not take arguments. If you want to create a computed property which does structural comparison, use @computed.struct
.
@computed.equals
for Custom Comparison
If you want to create a computed property which does custom comparison, use @computed.equals(comparer)
.
Built-in Comparers
MobX provides three built-in comparer
s which should cover most needs:
comparer.identity
: Uses the identity (===
) operator to determine if two values are the same.comparer.default
: The same ascomparer.identity
, but also considersNaN
to be equal toNaN
.comparer.structural
: Performs deep structural comparison to determine if two values are the same.
Note on Error Handling
If a computed value throws an exception during its computation, this exception will be caught and re-thrown any time its value is read. It is strongly recommended to always throw Error
's, so that the original stack trace is preserved (e.g. throw new Error("Uhoh")
instead of throw "Uhoh"
). Throwing exceptions doesn't break tracking, so it is possible for computed values to recover from exceptions.
const x = observable(3)
const y = observable(1)
const divided = computed(() => {
if (y.get() === 0)
throw new Error("Division by zero")
return x.get() / y.get()
})
divided.get() // returns 3
y.set(0) // OK
divided.get() // Throws: Division by zero
divided.get() // Throws: Division by zero
y.set(2)
divided.get() // Recovered; Returns 1.5