Categories
Mobx

Updating MobX Observables with Actions

Spread the love

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.

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 *