Categories
Mobx

Adding Computed Values to MobX Observable Classes

Spread the love

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 debugging
  • context — the this value that should be used in the provided expression
  • set — the setter runction to be used. We need this to assign new values to a computed value
  • equals — a comparison function for comparing the previous value with the new value. Defaults to comparer.default .
  • requiresAction — we should set this to true 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-evaluation
  • keepAlive — 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 same
  • comparer.default — same as comparer.identity , by considers NaN to be equal to NaN .
  • comparer.structural — performs a deep structural comparison to determine if 2 values are the same
  • comparer.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.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *