Categories
Mobx

Updating MobX Observables with Actions

We can update the values of MobX observables. To do this, we can use the action decorator or function.

In this article, we’ll look at how to use the action decoration and decorator.

Usage

The action decorator and function take a variety of form:

  • action(fn)
  • action(name, fn)
  • @action classMethod() {}
  • @action(name) classMethod () {}
  • @action boundClassMethod = (args) => { body }
  • @action(name) boundClassMethod = (args) => { body }
  • @action.bound classMethod() {}

Actions are anything that modifies the state. With MobX, we can make it explicitly in our code by marking them.

It takes a function that returns a function with the same signature, but it’s wrapped transaction , untracked , and allowStateChanges .

Applying translation increases the performance of actions, lets us batch mutation, and only notify computed values and reactions after the outermost actions has finished.

This makes sure that intermediate and incomplete values produced during action aren’t visible to the rest of the app until the action is done.

We can use action on any function that modifies observables or has side effects.

action also provides useful debugging information when we use the MobX dev tools.

Using action decorator with setters isn’t supported.

action is required when MobX is configured to require actions to make state changes with the enforceActions option.

When to use actions?

Actions should be used on functions that modify state. Functions that just performs look-ups and filters shouldn’t be marked as actions so that their invocation can be tracked.

Bound Actions

action.bound can be used to automatically bind actions to the targeted object. It doesn’t take a name parameter, so the name will always be based on the property name to which the action is bound.

Basic Example

We can use MobX actions as follows:

import { observable } from "mobx";

class Counter {
  @observable count = 0;

  @action.bound
  increment() {
    this.count++;
  }
}

const counter = new Counter();
counter.increment();

In the code above, we have the Counter class which has the count observable field.

To update the count , we have a bound action increment to update the count . Then we create a new Counter instance and call the increment method on it.

Writing Asynchronous Actions

Actions can also be used with a function that has asynchronous code like promises and setTimeout .

For example, we can use it as follows:

import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
  @observable joke = {};
  @observable state = "pending";

  @action
  async fetchJoke() {
    try {
      const response = await fetch("https://api.icndb.com/jokes/random");
      this.joke = response.json();
      mobx.runInAction(() => {
        this.state = "success";
      });
    } catch {
      mobx.runInAction(() => {
        this.state = "error";
      });
    }
  }
}
const joker = new Joker();
joker.fetchJoke();

In the code above, we used the action decorator to get a joke from the Chuck Norris API and then set it to our joke observable field.

Then we created a new instance of Joker and called the fetchJoke method in it.

The runInAction with its callback is needed to run actions after the await is done. Therefore, we set the new value of this.state in there.

We also have:

mobx.configure({ enforceActions: "observed" });

so that we can’t modify observable values outside actions.

flows

We can use flow s as an alternative to async and await . With flow s, we don’t have to call runInAction to change observable values after await is done.

It’s only available as a function.

For instance, we can rewrite the example as follows:

import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
  @observable joke = {};
  @observable state = "pending";

  fetchJoke = mobx.flow(function*() {
    try {
      const response = yield fetch("https://api.icndb.com/jokes/random");
      this.joke = response.json();
      this.state = "success";
    } catch {
      this.state = "error";
    }
  });
}
const joker = new Joker();
joker.fetchJoke();

The difference between this and the previous example is that we used flow , and that we passed in a generator function into it instead of an async function.

We also remove the runInAction calls since now we can set the observable values directly and the values will update correctly.

Promises with Then

If we use then , then we have to use action.bound on the callbacks we passed into then .

For example, we can write the following:

import { observable, action } from "mobx";
import * as mobx from "mobx";

mobx.configure({ enforceActions: "observed" });
class Joker {
  @observable joke = {};

  @action.bound
  fetchJokeSuccess(response) {
    this.joke = response.json();
  }

  @action
  fetchJoke() {
    this.joke = {};
   fetch("https://api.icndb.com/jokes/random").then(this.fetchJokeSuccess);
  }
}
const joker = new Joker();
joker.fetchJoke();

The code above will ensure that the correct this is applied to our fetchHJokeSuccess callback.

To make the code shorter, we can rewrite it as follows:

import { observable, action } from "mobx";
import * as mobx from "mobx";

mobx.configure({ enforceActions: "observed" });
class Joker {
  @observable joke = {};

  @action
  fetchJoke() {
    this.joke = {};
    fetch("https://api.icndb.com/jokes/random").then(
      action("fetchJokeSuccess", response => {
        this.joke = response.json();
      })
    );
  }
}
const joker = new Joker();
joker.fetchJoke();

In the code above, we passed in the action function with our callback directly to then .

Conclusion

We can create actions with the action decorator or function.

To create asynchronous actions, we can use async functions with the runInAction call.

We can also use the flow function with a generator passed in.

Categories
Mobx

Watching MobX Observables with the Reaction Function

We can create Observable objects with MobX. To watch it for changes, we can use the reaction function.

In this article, we’ll look at how to use the reaction function to watch for value changes in MobX Observables.

Reaction

We can use the reaction function as follows:

reaction(() => data, (data, reaction) => { sideEffect }, options?)

The reaction function is a variation on autorun that gives more fine-grained control on how an observable will be tracked.

The side-effect won’t be run directly when the observable is created. It’ll run only after the data expression returns a new value for the first time.

Any observables that are accessed while executing the side effect won’t be tracked.

reaction returns a disposer function.

The second function passed to reaction will retrieve 2 arguments when it’s run. The first argument is the value returned by the data function. The second argument is the current reaction during execution.

The side effect only reacts to data that was accessed in the data expression. It’ll only be triggered when the data returned by the expression has changed. This means that we have to produce things we need on our side effects.

Options

The third argument of reaction is an options object that can take the following optional options:

  • fireImmediately — boolean that indicates the effect function should trigger immediately after the first run of the data function. This is false by default.
  • delay — number of milliseconds that can be used to debounce the effect function. If it’s zero then no debouncing will happen
  • equals — a compare function that’ll be used to compare the previous and next values produced by the data function. The effect function will only be invoked if the function returns false . Default is comparer.default .
  • name — a string that’s used as the name for the reaction
  • onError — a function that’ll handle the errors of this reaction, rather than propagating them
  • scheduler — set a custom scheduler to determine how re-running the autorun function should be scheduled

Usage

We can use the reaction function as follows:

import { reaction, observable } from "mobx";

const todos = observable([
  {
    title: "eat"
  },
  {
    title: "drink"
  }
]);

reaction(
  () => todos.map(todo => todo.title),
  titles => console.log(titles.join(", "))
);

todos.push({ title: "sleep" });

In the code above, we created a new observable array using the observable function, which we assigned to the todos constant.

Then we call the reaction function, where we pass a callback to return an array which has the title strings from the todos observable array as the first argument.

In the second argument, we get the titles array returned from the function in the first argument, and the console.log the titles array joined together with join .

Then we call push on it, as we did in the last line, the new comma-separated string will be logged with the console.log since we changed it. It won’t log the value when it’s initially created.

Conclusion

We can use the reaction function to watch for observable variable changes.

It won’t watch for the value when it’s initially created.

reaction takes a callback to watch the value and return something we want from it. The second argument takes a value that takes the returned value from the first function and then we can perform some side effects on it.

The 3rd argument takes an object that takes a variety of options.

Categories
Mobx

Watching MobX Observables with the Autorun Function

We can create Observable objects with MobX. To watch it for changes, we can use the autorun function.

In this article, we’ll look at how to use the autorun function to watch for value changes in MobX Observables.

Autorun

mobx.autorun is used in cases where we want to create a reactive function that doesn’t have an observer itself.

It’s needed for situations like logging, persistence, or UI-updating code.

When autorun is used, the provided function will always be triggered once immediately and then again each time one of its dependencies changes.

computed creates functions that only re-evaluate if it has observers on its own. Therefore, autorun is useful for cases where the Observables don’t have its own observer.

The autorun function returns a disposer function, which is used to dispose of the autorun when we no longer need it.

The reaction itself will be passed as its only argument of the callback function we pass into autorun .

Therefore, we can dispose of autorun by writing:

const disposer = autorun(reaction => {});
disposer();

autorun(reaction => {
  reaction.dispose();
});

autorun will only observe data that are used during the execution of the provided function.

For example, we can write the following:

import { autorun, observable, computed } from "mobx";

const numbers = observable([4, 5, 6]);
const sum = computed(() => numbers.reduce((a, b) => a + b, 0));

const disposer = autorun(() => console.log(sum.get()));
numbers.push(7);

to watch the value of sum , which is a computed value derived from the numbers Observable array.

Options

The autorun function takes a variety of options that can be passed in as the properties of an object as the second argument.

They’re the following:

  • delay — number of milliseconds that can be used to debounce the callback function. Defaults to 0, which means no delay
  • name — string that’s used as the name in various situations like spy events
  • onError — function that’ll handle the errors of the reaction, rather than propagation them
  • scheduler — set a custom scheduler to determine how re-running autorun should be scheduled. The value is a function that has the run callback as a parameter like { scheduler: run => { setTimeout(run, 500) }} .

onError

We can use onError as follows:

import { autorun, observable } from "mobx";

const numFruits = observable.box(10);

const dispose = autorun(
  () => {
    if (numFruits.get() < 0) {
      throw new Error("numFruits should not be negative");
    }
    console.log("Age", numFruits.get());
  },
  {
    onError(e) {
      window.alert("Please enter a valid number");
    }
  }
);

numFruits.set(-1);
dispose();

In the code above, the line:

numFruits.set(-1);

will trigger the ‘Please enter a valid number’ alert to be displayed since we have:

if (numFruits.get() < 0) {
  throw new Error("numFruits should not be negative");
}

in the autorun callback. Therefore, the error will be caught by our onError handler.

We can also set a global error handler by setting the onReactionError function.

Conclusion

We can create an Observable and watch its value with the autorun function. It’s useful for Observables that don’t have an observe method.

It takes a callback with the reaction parameter that we can use to call dispose to dispose of the autorun.

The value of the Observable will be retrieved automatically if we reference it in the callback. As the value of the Observable changes, the autorun callback will run.

It also takes a variety of options like delay, onError handler, and more.

Categories
Mobx

Adding Computed Values to MobX Observable Classes

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.

Categories
Mobx

Creating and Using Mobx Observable Maps

Mobx is a state management solution for JavaScript apps. It lets us observe values from a store and then set the values, which will be immediately reflected in the store.

In this article, we’ll look at how to create and use Observable Maps with Mobx.

Observable Maps

Observable maps are a collection of key-value pairs that we can get and set. The value changes can be watched.

To create an Observable map, we can use the observable.map method as follows:

import { observable, autorun } from "mobx";

const values = {
  foo: 1,
  bar: 2
};

const observableMap = observable.map(values);

autorun(() => console.log(observableMap.toJS()));

In the code above, we defined an Observable map by creating an values object and then passing in values into the observable.map method.

It also optionally takes a second argument to specify various options.

We can also pass in an ES6 JavaScript Map instance as follows:

import { observable, autorun } from "mobx";

const values = new Map([["foo, 1"], ["bar", 2]]);
const observableMap = observable.map(values);
autorun(() => console.log(observableMap.toJS()));

In both examples, the autorun function watches the value of the Observable map as it changes.

It’ll log the value when it’s created in both examples.

A Mobx Observable map has the same methods as an ES6 Map and more. They’re the following:

  • has(key) — returns whether a map has an entey with the provided key.
  • set(key, value) — sets the given key to value . The provided key will be added to the map if it doesn’t exist yet
  • delete(key) — deletes the given key and its value from the map
  • get(key) — returns the value at the given key or undefined if the key isn’t found
  • keys() — returns an iterator to iterate through all the keys in a map in the order that they’re inserted
  • values() — returns an iterator to iterate through all values present in the map in the order that they’re inserted.
  • entries() — returns an iterator to iterate through all key-value pairs in the map with each entry being an array with the key and value.
  • forEach(callback) — runs the callback for each key/value pair in the map
  • clear() — removes all entries from a map
  • size — returns the number of entries in a map
  • toJS() — converts an Observable map to a normal map
  • toJSON() — returns a shallow plain object representation of the map. We can use mobx.toJS(map) to deep copy a map
  • intercept(interceptor) — registers an interceptor that’ll be triggered before any changes are applied to the map
  • observe(listener, fireImmediately?) — attaches a listener that listens to changes in the map
  • merge(values) — copy all entries from the provided object into the map
  • replace(values) — replaces the entire contents of the map with the provided values.

Options

The second argument takes some options to modify the Observable map’s behavior.

We can pass in { deep: false } to create a shallow map to prevent a nested Observable map from being created.

For example, if we write:

import { observable, autorun } from "mobx";
const values = {
  foo: 1,
  bar: {
    baz: 2
  }
};
const observableMap = observable.map(values);
autorun(() => console.log(observableMap.toJS()));

We get a nested Observable map since we have a nested object. The value of bar is an Observable map.

On the other hand, if we write:

import { observable, autorun } from "mobx";
const values = {
  foo: 1,
  bar: {
    baz: 2
  }
};
const observableMap = observable.map(values, { deep: false });
autorun(() => console.log(observableMap.toJS()));

Then we get the plain object:

{
  baz: 2
}

as the value of bar .

Another option is the { name: "my map" } option, which names the Observable map so we can identify it when we’re debugging with MobX dev tools.

Conclusion

We can create maps that are observable with MobX. It’s like a regular JavaScript map except that it has additional methods.

Also, we can watch is value and manipulate it like a regular map.

By default, Observable maps are recursive, so the values of an Observable map entry may also be an Observable map. We can set the deep option to false to disable this behavior and then the nested values will become a plain object.