Categories
Mobx

Watching MobX Observables with Intercept and Observe

We can watch MobX detect and modify mutations with the intercept function and watches observable value changes with the observe function.

In this article, we’ll look at how to use intercept to monitor changes and detect and modify mutations before they’re applied to an observable and use observe to watch for changes.

Intercept

We can call the intercept function as follows:

intercept(target, propertyName?, interceptor)

In the signature above, target is the observable to guard, propertyName is an optional parameter to specify specific properties to intercept.

intercept(user.name, interceptor) is different from intercept(user, "name", interceptor) . The first tries to add an interceptor to the current value inside user.name and the latter intercept changes to the name property of user .

The interceptor is a callback that’ll be run for each change that’s made to the observable.

The intercept function tells MobX what needs to happen with the current change.

It should do one of the following things;

  • Return the received change object as-is from the function
  • Modify the change object and return it
  • Return null to indicate the change shouldn’t be applied
  • Throw an exception

intercept returns a disposer function that can be used to cancel the interceptor when it’s invoked.

It’s possible to register multiple interceptors to the same observable. They’ll be chained in the registration order.

If one of the interceptors returns null or throw an exception, then the other interceptors won’t be evaluated anymore.

It’s also possible to register an interceptor both on a parent object and an individual property.

For example, we can use it as follows:

import { observable, intercept } from "mobx";

const theme = observable({
  color: "#ffffff"
});

const disposer = intercept(theme, "color", change => {
  if (!change.newValue) {
    return null;
  }
  if (change.newValue.length === 6) {
    change.newValue = `#${change.newValue}`;
    return change;
  }
  if (change.newValue.length === 7) {
    return change;
  }
  if (change.newValue.length > 10) {
    disposer();
  }
  throw new Error(`Invalid color`);
});

In the code above, we defined an interceptor for the color property of the theme observable object.

The interceptor callback takes a change parameter with the value passed in. Then it runs through some checks and makes some changes to the newValue property.

Observe

The observe function is used to watch for changes of an observable.

It can be called as follows:

observe(target, propertyName?, listener, invokeImmediately?)

In the signature above, the target is observable to watch.

propertyName is an optional parameter to specify a property to observe. observe(user.name, listener) is different from observe(user, "name", listener) . The first observes the current value in user.name and the latter observes the name property of user.

listener is a callback that’ll be invoked for each change that’s made to the observable. It receives a single change object describing the mutation except for boxed observables, which will invoke a listener with the newValue and oldValue parameters.

invokeImmediately is a boolean that’s false by default. Set it to true if we want observe to invoke listener directly with the state of the observable instead of waiting for the first change.

For example, we can use it as follows:

import { observable, observe } from "mobx";

const person = observable({
  firstName: "John",
  lastName: "Smith"
});

const disposer = observe(person, change => {
  console.log(
    `${change.type} ${change.name} from ${change.oldValue} to ${
      change.object[change.name]
    }`
  );
});

person.firstName = "Jane";

In the code above, we have a callback with the change object that has the type of change, name of the change, the oldValue and the change.object[change.name] to get the new value.

Events

The callbacks of intercept and observe will receive an event object which has the object for the observable triggering the event and the type string for the type of the current event.

They also have additional fields depending on the type:

Object add Event

add event gives us the name and newValue properties for the name of the property being added and the new value being assigned respectively.

Object update Event

update event gives us the name and newValue properties for the name of the property being added and the new value being assigned respectively.

In addition, it gives us the oldValue property for the value that’s being replaced.

Array splice Event

This gives us index for the starting index of the splice. Splices are also fired by other array methods like push etc.

removedCount gives us the amount of items being removed.

added gives us an array of items being added.

removed gives us an array with items being added.

addedCount gives us the number of items that were added.

Array update Event

The update event gives us index which has the index of the single entry that’s being updated

newValue gives us the value that’s being assigned.

oldVale has the old value that’s being replaced.

Map add and delete Events

The Map add and delete events has the name and newValue properties for the name of the property being added and the new value being assigned respectively.

Map update Event

The delete event gives us the properties that are sent with add and delete events and also has the oldValue with the value that’s being replaced.

Boxed & computed observables create Events

The create event for boxed and computed observables has the newValue property to get us the value that we assigned during creation.

Boxed & computed observables update Events

The update event also gives us the oldValue with the value being replaced in addition to the newValue .

Conclusion

We can use the intercept function to do something before the mutation of the observable’s state is complete.

The observe function can be used to watch for value changes in a MobX observable. It takes a callback that gives us old and new values of the state and other information.

Categories
Mobx

List of MobX Modifiers

MobX has a set of decorators to change how observable properties will behave.

In this article, we’ll look at them one by one and see how we can use them.

Modifiers

MobX comes with the following decorators that defines how observable properties will behave:

  • observable — an alias for observable.deep
  • observable.deep — the default modifier used by any observable. It clones and converts any array, map or plain object into its observable counterpart
  • observable.ref — disables automatic observable conversion and creates an observable reference instead
  • observable.shallow — can only be used with collections. Turns any assigned collection into an observable, but the values will be treated as-is
  • observable.struct — like ref but ignore new values that are structurally equal to the current value
  • computed — creates a derived property
  • computed(options) — same as computed , but sets additional options
  • computed.struct — same as computed , but only notify any of is observers when the value produced is structurally different from the previous value.
  • action — creates an action
  • action(name) — creates action and overrides the name
  • action.bound — creates action and binds this to the instance.

Decorators can be used the MobX’s decorate , observable.object , extendObservable and observable to specify how object members should behave.

observable.deep is the default behavior for any key-value pair by default and computed for getters.

For example, we can define an observable as follows:

import { observable, action } from "mobx";

const person = observable(
  {
    firstName: "John",
    lastName: "Smith",
    age: 42,

  get fullName() {
      return `${this.firstName} ${this.lastName}`;
    },

  setAge(age) {
      this.age = age;
    }
  },
  {
    setAge: action
  }
);

In the code above, we have the default decorators for all members except for setAge , which we explicitly defined as an action .

Therefore, firstName , lastName , and age are observable s and fullName is a computed field.

We can use the decorate function as follows:

import { observable, action, decorate } from "mobx";

class Person {
  firstName = "John";
  lastName = "Smith";
  age = 42;

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  setAge(age) {
    this.age = age;
  }
}

decorate(Person, {
  firstName: observable,
  lastName: observable,
  age: observable,
  setAge: action
});

In the code above, we call decorate with the class as the first argument, and then an object with the fields we want to modify as the second argument.

We set firstName , lastName and age as observables and setAge as an action.

fullName is a computed field since it’s the default option for getters.

Deep Observability

When MobX creates an observable object using observable , observable.object or extendObservable , it introduces observable properties which use the deep modifier by default.

The deep modifier recursively calls observable(newValue) for any assigned value which uses the deep modifier until it gets to the bottom level of the object.

Reference Observability

In some cases, objects don’t need to be converted into observables. For example, we don’t want to do this for immutable objects or objects that are managed by an external library.

In this case, we can use the ref modifier.

For example, we can use it as follows:

class Person {
  firstName = "John";
  lastName = "Smith";
  @observable.ref age = 42;
}

In the code above, we added the observable.ref decorator to age so that MobX will only track its reference but doesn’t try to convert its value.

With ES5 syntax, we can write the following:

import { observable, extendObservable } from "mobx";

function Person() {
  extendObservable(
    this,
    {
      name: "Joe",
      age: 42
    },
    {
      age: observable.ref
    }
  );
}

Shallow Observability

We can use the observable.shallow modifier to apply observability one level deep. We need this to create a collection of observable references.

It won’t recursively apply observability like deep .

For example, we can use it as follows:

class Books {
  @observable.shallow authors = [];
}

{ deep: false } can be passed as an option to observable , observable.object , observable.array , observable.map , and extendObservable to create shallow collections.

Conclusion

We can use modifiers to change the way MobX watches the changes in the values.

The default is that it looks at a value recursively for changes.

There’re also modifiers for computed values, shallow watch, and more.

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.