Categories
Mobx

Reacting to Observables with MobX-React

We can use MobX with MobX-React to manage the state by watching MobX observables and changing the observable data.

In this article, we’ll look at how to create an Observable and use it in React components directly.

Creating Observables and Using Them in React Components

We have to install the mobx and mobx-react packages to create Observables and use them within React components.

To do this, we can write the following:

npm i mobx mobx-react

Then we can create our Observable object and a React component that uses it as follows:

import React from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

const App = observer(({ countData }) => (
  <>
    <button onClick={() => countData.count++}>Increment</button>
    <p>{countData.count}</p>
  </>
));
const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

In the code above, we created the countData Observable object by writing:

const countData = observable({
  count: 0
});

We’ll be able to get the latest state within our React component and then pass it in as a prop for our React component.

To make our React component watch for the latest value from our Observable object and let us change its value from within the component, we write:

const App = observer(({ countData }) => (
  <>
    <button onClick={() => countData.count++}>Increment</button>
    <p>{countData.count}</p>
  </>
));
const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

The code above gets the countData prop from the countData Observable. Then in the onClick handler, we just pass in the function to change its value.

Then when we reference the App component in the last line, we just pass in the countData Observable object as the value of the countData prop.

countData.count++ will change the state of the Observable, which then the latest value will be obtained from the prop.

We can use the countData Observable object with class components as follows:

import React from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

@observer
class App extends React.Component {
  render() {
    return (
      <>
        <button onClick={() => countData.count++}>Increment</button>
        <p>{countData.count}</p>
      </>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

The only difference is that it’s a class component with a render method and that we use the observer decorator instead of the observer function.

Using Context to Pass Observables Around

We can use the React Context API to pass the values of an Observable to another component.

For instance, we can write the following code:

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const countData = observable({
  count: 0
});

const CountContext = React.createContext();

const Counter = observer(() => {
  const countData = useContext(CountContext);
  return (
    <>
      <button onClick={() => countData.count++}>Increment</button>
      <p>{countData.count}</p>
    </>
  );
});

const App = ({ countData }) => (
  <CountContext.Provider value={countData}>
    <Counter />
  </CountContext.Provider>
);

const rootElement = document.getElementById("root");
ReactDOM.render(<App countData={countData} />, rootElement);

In the code above, we have the CountContext . We create it by writing:

const CountContext = React.createContext();

Then in App , we included the CountContext component, so that we can get the countData Observable value by passing in the countData prop with the value set to the countData Observable.

In the Counter component, we call the observer function with a function component inside.

Inside the component, we use the useContext hook to get the value from CountContext .

Then in the handler function, we passed into the onClick prop of the button, we changed the count ‘s value, which will automatically change the value in the countData Observable object and set as the value of countData.count .

Therefore, when we click the Increment button, the number below it will go up.

Storing Observables in Local Component State

We can also use Observables as a local state in a React component.

To do this, we can use the useState hook by passing in a function that returns an Observable object as follows:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

const App = observer(() => {
  const [countData] = useState(() =>
    observable({
      count: 0
    })
  );

  return (
    <>
      <button onClick={() => countData.count++}>Increment</button>
      <p>{countData.count}</p>
    </>
  );
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

In the code above, we used the useState hook by passing in a function that returns an Observable object with the count property. We then assigned that to the countData variable.

Once we did that, countData.count will be updated by the onClick handler and when we click the Increment button, we’ll see the countData.count value update automatically.

If we click the Increment button, then the value will go up.

We don’t really need to use MobX Observable objects to store local state unless complex computations are involved, since MobX will optimize for those.

Likewise, we can use MobX to store local state with class components as follows:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable } from "mobx";

@observer
class App extends React.Component {
  @observable count = 0;

  render() {
    return (
      <>
        <button onClick={() => this.count++}>Increment</button>
        <p>{this.count}</p>
      </>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

In the code above, we have this.count , which is an Observable field. In the onClick listener, we just modify this.count directly, and then it’ll be reflected in the this.count in between the p tag.

@observer implements memo orshouldComponentUpdate automatically so that there won’t be any unnecessary re-renders.

Conclusion

We can use MobX Observable to store a React app’s state or a React component’s state.

To use it for storing React app’s state, we can either pass an Observable to a component as a prop or we can use the Context API to send the data to different components.

We can also use it to store local state by either using the useState hook in function components and using the observable decorator within a class component.

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.