In this article, we’ll look at how to use the computed
decorator to derive computed values from existing states.
computed Decorator
Computed properties are derived from other Observable properties of a class. It won’t be computed again if none of the Observable values that derive the computed property has changed.
Computed properties aren’t enumerable, nor can they be overwritten in an inheritance chain.
To create a computed property, we can write the following:
import { observable, computed } from "mobx";
class Person {
@observable firstName = "Jane";
@observable lastName = "Smith";
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
In the code above, the @computed
decorator decorates the fullName
getter property, which returns the combination of firstName
and lastName
.
We can also use the decorate
function to create computed properties as follows:
import { decorate, observable, computed } from "mobx";
class Person {
firstName = "Jane";
lastName = "Smith";
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
decorate(Person, {
firstName: observable,
lastName: observable,
fullName: computed
});
The code above is the same as using decorators.
We can also define getters for objects:
import { observable } from "mobx";
const person = observable.object({
firstName: "Jane",
lastName: "Smith",
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
});
Then fullName
will be watched like any other Observable object properties.
We can also define setters as follows:
import { observable, computed } from "mobx";
class Person {
@observable firstName = "Jane";
@observable lastName = "Smith";
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@computed get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fn(value) {
this.firstName = value;
}
}
In the code above, the fn
method is our setter function for setting the firstName
property.
computed(expression)
as a Function
MobX also comes with a computed
function which takes a callback that returns the value we want.
For example, we can use it as follows:
import { observable, computed } from "mobx";
const name = observable.box("Jane");
const lowerCaseName = computed(() => name.get().toLowerCase());
const disposer = lowerCaseName.observe(change => console.log(change.newValue));
name.set("Joe");
In the code above, we created the name
Observable string with the initial value 'Jane'
and then we created a computed value called lowerCaseName
.
In the callback we passed into the computed
function, we return the name
‘s value converted to lower case.
Then we attached a listener to watch for changes in lowerCaseName
.
When we set the value of name
, the callback in computed will run and then the change listener for lowerCaseName
also runs as the value changes.
Options for computed
computed
takes a second argument with various options that we can set to change the computed value’s behavior.
They’re the following:
name
— a name we set and used for debuggingcontext
— thethis
value that should be used in the provided expressionset
— the setter runction to be used. We need this to assign new values to a computed valueequals
— a comparison function for comparing the previous value with the new value. Defaults tocomparer.default
.requiresAction
— we should set this totrue
on very expensive computed values. If we try to read its value but the value isn’t tracked by some observer, then the computed property will throw an error instead of doing a re-evaluationkeepAlive
— don’t suspend the computed value if it’s not observed by anything. This can easily lead to memory leaks.
Built-in comparers
MobX has the following built-in comparer
s:
comparer.identity
— uses the===
operator to determine if 2 values are the samecomparer.default
— same ascomparer.identity
, by considersNaN
to be equal toNaN
.comparer.structural
— performs a deep structural comparison to determine if 2 values are the samecomparer.shallow
— perfoms shallow structural comparison to determine if 2 values are the same
Error Handling
If a computed value throws an exception during its computation, it’ll be caught and rethrown any time its value is read. To preserve the original stack trace, we should use throw new Error('...')
.
For example, we can write the following:
import { observable, computed } from "mobx";
const x = observable.box(3);
const y = observable.box(1);
const divided = computed(() => {
if (y.get() === 0) {
throw new Error("Division by zero");
}
if (isNaN(y.get())) {
throw new Error("Not a number");
}
return x.get() / y.get();
});
divided.get();
y.set(0);
divided.get();
y.set(2);
divided.get();
We throw errors in the computed
function’s callback.
Computed Values with Arguments
We can use the mobx-utils
package’s computedFn
function to create a computed function that takes an argument.
For example, we can use it as follows:
import { observable } from "mobx";
import { computedFn } from "mobx-utils";
class Todos {
@observable todos = [];
getAllTodosByName = computedFn(function getAllTodosByName(name) {
return this.todos.filter(todo => todo === name);
});
}
const todos = new Todos();
todos.todos.push("eat");
console.log(todos.getAllTodosByName("eat"));
In the code above, we have:
getAllTodosByName = computedFn(function getAllTodosByName(name) {
return this.todos.filter(todo => todo === name);
});
which creates a getAllTodosByName
method that can be called and get the returned value like any other method.
The last console.log
line will get us the value. We have to use a traditional function as the computedFn
callback so that we get the correct value for this
.
Conclusion
We can use the computed
decorator to create computed values in classes.
We can add setters to set the value of a computed value.
The computed
function takes a callback that returns an Observable entity that we can watch and manipulate. It takes a callback that returns the value that we want to derive from an existing Observable value.
The computedFn
function takes a callback to return the value we want and returns a function that we can call to get that value.