MobX observables and actions

Anton Ioffe - September 11th 2023 - 14 minutes read

In today’s digital age, crafting a sophisticated web application is more than just piecing together HTML tags or animating components with CSS; it’s fundamentally about creating elegant interactions fueled by efficiently managed data. For JavaScript developers, one of the aiding tools in this quest is MobX - a powerful state management library. Throughout this article, we will delve deep into the mechanics of MobX, exploring its dynamic features that help ease the complexity of data management in modern web development.

As we navigate the intricacies of MobX, we will illuminate the roles and tangible advantages of Observables, Actions, and Observers in state management. We'll also feature high-quality, annotated code examples that show in detail how these features are implemented and utilized in real-world scenarios.

Towards the conclusion, the narrative will unlock some of MobX’s advanced concepts such as AutoRun, runInAction and Reaction, unveiling the subtle differences and nuances between them. Each of these functions has its unique role in managing reactivity in complex applications, but how do they work in collaboration and when does one function serve better than the other? Stay put as we unravel the answers and delve into discussions around their practical implications and best practices.================================================

Understanding MobX and its role in modern web development

MobX is a reactive state management library employed in contemporary web development. It's built upon the reactive programming concepts, which focus on asynchronous changes propagation and data streams over time. MobX assists in building simple, scalable, and maintainable applications by enabling the application state to automatically update the UI when a change occurs. Therefore, one can observe that MobX plays a vital role in effectively managing state in modern web development.

The mechanics of MobX mostly revolve around two indispensable parts: observables and actions. Here we will be focusing solely on the general role and benefits of MobX, avoiding details on observables and actions.

Working Principles

MobX operates on very simple principles. Here is a summary of the main points:

  1. State is the heart: The state of your app should be the definitive single source of truth
  2. Derivations: Everything that can be derived from the state, should be derived automatically
  3. Atomic updates: Minimize the state changes to the strict minimum

A simple, real-world code example would look like this:

import { observable } from 'mobx';

// State as the heart
class Store {
    @observable items = [];
}

const store = new Store();

// Atomic update
store.items.push({ name: 'mobx' });

In the above code, the state (items) is observed and updated only when a change occurs. In this case, updating items will automatically invoke any computed values or reactions that rely on items.

Common Pitfalls

One common mistake made by developers new to MobX is mutating observables outside of actions. While this might not immediately seem problematic, it can lead to subtle bugs over time. An example of this incorrect practice is shown below.

store.items[0].name = 'react'; // NOT advisable

The correct approach is to always use actions for any state mutations.

import { action } from 'mobx';

class Store {
    @observable items = [];

    @action
    addItem(item) {
        this.items.push(item);
    }

    @action
    editItem(index, name) {
        this.items[index].name = name;
    }

}

Doing so allows MobX to batch updates, ensuring that change notifications are propagated atomically, thereby maintaining consistency.

Why Choose MobX?

What makes MobX stand out in the crowded arena of state management solutions? Well, it's the trade-off between simplicity and functionality that MobX offers. It's easier to understand than Redux because it requires less boilerplate code. On the other hand, it also offers more flexibility than useState and useContext hooks.

Simultaneously, MobX can handle complex state shapes, which allows for better code structure. Modularity and reusability are another key selling point. The code is readable and more maintainable due to automated state management.

In summary, MobX is an efficient and effective state management library for sophisticated JavaScript applications. By making the data flow in your app more explicit and minimizing state changes, you can produce a cleaner and more maintainable code base. Next time you face the challenge of handling a complex state, consider giving MobX a try.

Exploring Observables in MobX: Declaration and Usage

Observables, a cornerstone of MobX, are pivotal for automatic tracking of changes to designated properties and objects. This in-depth analysis will elucidate the declaration, application, and substantial performance benefits of observables.

Declaring Observables in MobX

Observables in MobX are simply parts of the application state that, once declared, prompt MobX to monitor changes to the relevant property or object. Observe this in practice:

import { observable } from 'mobx';

class Store {
    @observable count = 0; // Declaring 'count' property as observable
}

Above, we've made count observable so MobX will watch for updates. A common slip-up is omitting the decorator syntax during declaration:

import { observable } from 'mobx';

class Store {
    count = 0; // Incorrect! This is just an ordinary property not being watched
}

To avoid this, always utilize the observable decorator as shown in the initial example.

Using Observables in MobX

Employing observables is straightforward—any change propels an immediate reaction from observers. Let's delve into the use of observables:

import { observable } from 'mobx';

class Store {
    @observable count = 0; // Observable declared

    increment() {
        this.count++; // The value of 'count' gets incremented each time increment() is invoked
    }
}

Here, count is an observable that sparks immediate reactions from dependent components upon modification due to its observable status.

A prevalent error in dealing with observables is referencing them in the absence of the this keyword:

increment() {
    count++; // Incorrect
}

Always use this when referencing observables and other class properties:

increment() {
    this.count++; // Correct usage
}

Performance Benefits of Using Observables

Adopting observables can substantially optimize performance in larger applications. They activate only the dependent components or computations, preventing superfluous re-renders, consequently, enhancing performance.

Observables play a key role in writing efficient, reactive JavaScript code, a process made intuitive and straightforward by MobX. Equipped with newfound knowledge of MobX observables, you are primed to build high-performance applications!

Delving into Actions in MobX: Definition and Implementation

Before diving into actions, it's crucial to understand that MobX follows the fundamental principle of actions modifying the state, and reactions to those changes updating the frontend. With that said, actions in MobX refer to any piece of code that modifies the observable state.

Defining Actions in MobX

In MobX, we define actions using the action decorator. An action is typically a function that manipulates the state of observables. Here's how it works:

import { observable, action } from 'mobx';

class Store {
    @observable count = 0;

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

In the example above, the increment function is an action that modifies the observable state - the count property of the Store class.

Remember, a single action should ideally represent one logical unit of work, which could be changing a single observable's value or tweaking multiple observables. This approach ensures modularity and keeps your code highly organized.

Implementing Actions

While implementing actions, MobX provides both synchronous and asynchronous options. The traditional choice was to use runInAction for asynchronous behavior, but since MobX 4, you can now create asynchronous actions directly using the action decorator itself.

import { observable, action } from 'mobx';

class Store {
    @observable count = 0;

    @action.bound
    async complexIncrement() {
        const newValue = await someAsyncCall();
        this.count += newValue;
    }
}

In the asynchronous action complexIncrement, we are performing an asynchronous call and using its result to update the count observable. You might have also noticed the @action.bound syntax which binds the context of this within the action to the instance of the class.

Common Mistakes & Best Practices

Not using @action.bound with asynchronous actions

One common mistake made by developers while dealing with asynchronous actions is to forget to bind the context of this using @action.bound. When neglected, the code produces an error because this refers to the wrong context. To correctly express an asynchronous action, always remember to use @action.bound.

Incorrect approach:

import { observable, action } from 'mobx';

class Store {
    @observable count = 0;

    @action
    async increment() {
        setTimeout(() => {
            this.count++; // Throws an error, as 'this' doesn't refer to an instance of the class
        }, 1000);
    }
}

Correct approach:

import { observable, action } from 'mobx';

class Store {
    @observable count = 0;

    @action.bound
    async increment() {
        setTimeout(() => {
            this.count++;
        }, 1000);
    }
}

Overcrowded Actions

An action should perform one logical unit of work. Overpopulating an action with too many tasks reduces code modularity and readability.

Incorrect approach:

@action
largeAction(){
    this.a = true; // Many changes in
    this.b = 'newValue'; // a single action
    this.c++;
    // ... and more
}

Correct approach:

@action
setA(){
    this.a = true; // Single logical change per action makes code more readable
}

@action
setB(){
    this.b = 'newValue';
}

@action
incrementC(){
    this.c++;
}

In conclusion, understanding and correctly utilizing actions in MobX can significantly streamline your state management logic and make your code more modular and reusable. Scandinavian mentalities apply to your JavaScript coding as well - keep it simple, clean, and efficient! Now, ask yourself, are you designing your actions to be as simple and modular as they can be?

Observers in MobX: Their purpose and usage

MobX, a renowned state management library, adopts a unique strategy for managing state transitions in modern web development through the use of observables and observers. Observers, a core feature of MobX, are components or derivations that neatly track changes in the state, effectively responding to them automatically. This section offers a detailed overview of observers, including their purpose, usage, and how they interact with observables, bolstered with useful, real-world code examples.

Observers and Their Role

Observers in MobX serve as reactive components that respond to changes in the data they monitor. When an observable state undergoes a change, observers are promptly notified and deliver an immediate response. The ability of observers to track state transitions and effectively react forms the foundation of MobX's reactivity system.

One significant advantage of utilizing observers in MobX is their ability to ensure components only re-render when necessary. This improves application performance by reducing unneeded re-rendering. However, it's important to comprehend potential performance issues from incorrect usage. This includes tracking more observables than necessary, detecting irrelevant changes, or monitoring observables that never alter. So, having a solid understanding of how and when to correctly utilize observers is key to avoiding these problems.

Let's examine a typical scenario where a component, an observer by nature, observes the changes in an observable:

import { makeAutoObservable, autorun } from 'mobx';

// Declare AppState class with a title observable and updateTitle action
class AppState {
  title = "Hello, World";

  constructor() {
    makeAutoObservable(this);  // This function automatically infers which properties are meant to be observable, action or computed.
  }

  updateTitle(newTitle) {
    this.title = newTitle;
  }
}

// Create an instance of AppState
const appState = new AppState();

// Component that observes the title
class TitleComponent {
  render() {
    return `<h1>${appState.title}</h1>`;
  }
}

// Instantiate TitleComponent
const titleComponent = new TitleComponent();

// Track the state and re-render whenever the title changes
autorun(() => titleComponent.render()); // TitleComponent as an observer

In this code sample, appState.title is an observable state. When this state undergoes change, the autorun() function facilitates a re-run of titleComponent.render(), hence making TitleComponent an observer.

Common Mistakes in Observers Usage

Developers could stumble upon some typical errors when using observers.

Subscribing Observers to Non-observable Data

In MobX, observers can only subscribe to observable data. So, registering observers to non-observable data is a common blunder. For example, in the following incorrect code snippet, the reaction doesn't respond to changes in appState.title since it's not declared as an observable.

// WRONG: Attempting to observe non-observable data 
let appState = {
    title: 'Hello, World!'
};

autorun(() => {
    console.log(appState.title);  // appState.title isn’t observable, so the autorun function cannot observe it
});

The correct approach is to declare appState as observable:

// CORRECT: Observing observable data
class AppState {
  title = "Hello, World";

  constructor() {
    makeAutoObservable(this);  // appState.title is now observable
  }
}

const appState = new AppState();

autorun(() => {
    console.log(appState.title);  // autorun function can effectively observe appState.title
});

You should use the makeAutoObservable() function to convert your data into observable data, which effectively allows observers to function properly.

Ignoring Observer Dependencies

Another frequent pitfall is overlooking the dependencies of an observer. If an observer does not use any observables, it remains unresponsive to changes. For example, in the subsequent flawed code snippet, the autorun() function is not dependent on appState.title and thus doesn't react to its modifications.

let appState = {
    title: 'Hello, World!'
};

autorun(() => {
    console.log('Hello, MobX!');  // WRONG: autorun function doesn’t depend on any observable
});

To rectify this, make sure to include appState.title in the autorun() function:

class AppState {
  title = "Hello, World";

  constructor() {
    makeAutoObservable(this); // appState.title is declared observable
  }
}

const appState = new AppState();

autorun(() => {
    console.log(`Title: ${appState.title}`);  // RIGHT: autorun function is now dependent on appState.title
});

By making this change, autoRun() is now dependent on appState.title and will react whenever this value is altered. Each observer needs to depend on at least one observable to effectively react to changes.

Best Practices for Observer Usage

It's vital to employ proper practices in using observers with MobX. Here are a few crucial points to keep in mind:

  1. Always ensure that your data is marked as observable before attempting to observe it. As we've seen, observers don't respond to non-observable data.

  2. Observers should be dependent on at least one observable. An observer that is not dependent on any observables remains idle and unresponsive to changes.

  3. Keep your application performance in check by not overutilizing observers. Avoid observing more data than necessary and unnecessary re-rendering.

With the correct implementation, observers can be a powerful tool in managing application state. Understanding their usage and continually refining it can make your application more effective and efficient.

Understanding AutoRun, runInAction and their Nuances with Reaction in MobX

MobX offers several powerful functions to efficiently manage state in JavaScript applications - most notably, autorun, runInAction, and reaction. We will deeply explore these three critical functions, elaborating on their distinct functions, what makes their mechanism work, and their ideal uses for managing reactivity.

Unraveling AutoRun

The MobX function autorun establishes a reaction that is automatically triggered each time a dependent observable property is modified. Underneath the hood, autorun tracks all the observable properties it depends on during its first execution, thus creating a dependency tree which it monitors for alterations. It is a core attribute of MobX that offers simplicity, impeccable readability, and a concise syntax.

Consider the following typical autorun utilization:

import { observable, autorun } from 'mobx';

let myObservable = observable({
    name: 'Mobx',
    version: 6
});

autorun(() => {
    console.log(`My preferred JS library is ${myObservable.name}, version ${myObservable.version}`);
});

In this segment, the autorun function sets up an automatic reaction to log the message every time the name or version property of myObservable changes.

A Common Slip with AutoRun

One frequent misstep is triggering side effects corresponding to state modifications within autorun. While occasionally this may function as expected, it may also lead to unforeseen reactions and consequential errors. Thus, it is superior practice to keep the autorun function completely reactionary.

Incorrect manner:

autorun(() => {
    myObservable.version++;
});

Correct manner:

import { action } from 'mobx';

const incrementVersion = action(() => {
    myObservable.version++;
});

In this scenario, instead of manipulating the state within autorun, we construct an action named incrementVersion to handle the state modification.

Examining RunInAction

The MobX function runInAction empowers developers to consolidate multiple state modifications into a singular action occurring outside the UI, thereby heightening uniformity and predictability within our code.

Here's runInAction in effect:

import { observable, runInAction } from 'mobx';

let myObservable = observable({
    name: 'Mobx',
    version: 6
});

runInAction(() => {
    myObservable.name = 'React';
    myObservable.version = 16;
});

In this instance, runInAction adjusts both myObservable.name and myObservable.version in a synchronized and atomic operation.

Maximize Performance with RunInAction

A common mistake when leveraging MobX is causing superfluous reactions due to multiple state modifications. Such redundancy can negatively impact application performance. By bundling state modifications utilizing runInAction, we can alter multiple properties in a single transaction, yielding just one reaction and increasing efficiency.

Inefficient approach:

myObservable.name = 'React';
myObservable.version = 16;

Efficient approach:

runInAction(() => {
    myObservable.name = 'React';
    myObservable.version = 16;
});

Dissecting Reaction

MobX's reaction is designed for cases where creating a side effect based on specific observables is desirable. The reaction function takes in two separate functions as arguments: the first to observe and the second to respond.

Consider the ensuing use case for reaction:

import { observable, reaction } from 'mobx';

let myObservable = observable({
    name: 'Mobx',
    version: 6
});

reaction(
    () => myObservable.version,
    version => console.log(`Version changed to: ${version}`),
);

In this situation, the console will log the revised version each time myObservable.version changes.

Bolster Modularity with Reaction

A frequent fallacy is incorporating excessive observables in the reaction tracking function. This overcomplication diminishes the modularity and reusability of our code. It is prudent to ensure the tracking function precisely observes properties necessary for reactions, thereby fostering clean, efficient, and modular code.

Overly complex code:

reaction(
    () => myObservable,
    data => console.log(`Data altered: ${JSON.stringify(data)}`),
);

Highly modular code:

reaction(
    () => myObservable.version,
    version => console.log(`Version revised to: ${version}`),
);

How might you contemplate the application of these functions within the framework of a complex application? How do the varying applications of each function shape structural decisions? What potential advantages or disadvantages could arise from various strategies? Thorough comprehension of these aspects can distinctively contribute to developing flexibility and proficiency as a developer.

Summary

This article provides a comprehensive examination of JavaScript's state management library, MobX and focuses on crucial features like observables and actions. While highlighting the vital role and benefits of MobX in modern web development, it provides high-quality annotated code examples demonstrating the implementation of these features. It also discusses advanced concepts like AutoRun, runInAction, and Reaction, and outlines some of the common mistakes developers make along with ways to avoid them.

One of the key takeaways from the article is how MobX streamlines the management of state changes in abstraction levels. Understanding these complexities and implementing accordingly is a fundamental prerequisite for efficient web development. Furthermore, it's important to appreciate the benefits of using observables and actions in MobX, such as reducing unnecessary re-renders, automatic tracking of changes, minimizing state changes, and more.

Challenge task: To apply these concepts, create a sample JavaScript web application implementing MobX with observables and actions. Ensure to include the advanced functions AutoRun, runInAction, and Reaction within the application. Evaluate the performance before and after using MobX features. Ensure to make multiple state modifications using runInAction and observe the difference. Remember to avoid common mistakes made when utilizing these functionalities.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers