Skip to main content

Command Palette

Search for a command to run...

MobX State Management-Simple and Powerful State Machine

Updated
9 min read
MobX State Management-Simple and Powerful State Machine

MobX is a library for building responsive data models, offering a declarative approach to state management that automatically updates dependent views when data changes.

Creating Observable State

MobX uses the @observable decorator to create observable objects, arrays, or primitive types. When these change, observers depending on them are automatically updated.

import { observable } from 'mobx';

class Todo {
  @observable title;
  @observable completed;

  constructor(title) {
    this.title = title;
    this.completed = false;
  }
}

const todo1 = new Todo("Learn MobX");

Responsive Computed Values

The @computed decorator creates computed values based on other observables, automatically updating when dependencies change.

class TodoList {
  @observable todos = [];
  @computed get completedCount() {
    return this.todos.filter(todo => todo.completed).length;
  }
}

const todoList = new TodoList();
todoList.todos.push(todo1);

Actions

The @action decorator ensures state changes occur in a controlled environment, preventing unintended modifications.

class TodoList {
  // ...
  @action addTodo(title) {
    this.todos.push(new Todo(title));
  }

  @action toggleTodo(index) {
    this.todos[index].completed = !this.todos[index].completed;
  }
}

Observers

In React, use the mobx-react library’s observer higher-order component or useObserver hook to make components reactive to state changes.

import React from 'react';
import { observer } from 'mobx-react-lite';

const TodoListComponent = observer(({ todoList }) => (
  <ul>
    {todoList.todos.map((todo, index) => (
      <li key={index}>
        {todo.title} - {todo.completed ? 'Completed' : 'Not completed'}
      </li>
    ))}
  </ul>
));

const App = () => {
  const todoList = new TodoList();
  return <TodoListComponent todoList={todoList} />;
};

Reactive Programming

MobX’s core is its reactive system, where data changes automatically propagate to dependent computed values and observers without manual setState calls. Reactive programming emphasizes data flow and change propagation, enabling programs to respond to data changes automatically.

Observables

MobX uses the @observable decorator or observable function to create observable values. When these change, any dependent computations or views update automatically.

import { observable } from 'mobx';

class Counter {
  @observable count = 0;
}

const counter = new Counter();

// Observer updates automatically
autorun(() => {
  console.log(`Current count: ${counter.count}`);
});

// Modify count
counter.count++;

Computed Values

The @computed decorator creates computed values based on observables, updating automatically when dependencies change.

class Counter {
  // ...
  @computed get isEven() {
    return this.count % 2 === 0;
  }
}

// Computed value updates automatically
autorun(() => {
  console.log(`Is count even: ${counter.isEven}`);
});

// Modify count
counter.count++;

Reactions

Use autorun, reaction, or when functions to create reactions that execute automatically when data changes. autorun runs whenever dependencies change, while reaction runs on specific condition changes.

// Reactive UI update
reaction(
  () => counter.count,
  (newCount) => {
    updateUI(newCount);
  },
);

// Conditional reaction
when(
  () => counter.count > 10,
  () => {
    console.log('Count exceeds 10!');
  },
);

Actions

The @action decorator or action function marks functions that modify state, ensuring changes occur in a controlled environment to prevent unintended side effects.

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

counter.increment();

Automatic Dependency Tracking

MobX uses proxies and the visitor pattern to automatically track dependencies for computed values and reactions, eliminating manual dependency management.

Dependency Tracking

MobX uses proxies and the visitor pattern to track which computed values and observers depend on observable state, enabling efficient updates.

Proxies

MobX leverages ES6 Proxy objects to create proxies for observable objects. Proxies intercept access and modification operations, allowing MobX to detect when observable state is read or modified.

const observableValue = observable(42);
const proxyValue = new Proxy(observableValue, mobxHandler); // mobxHandler contains interception logic

Visitor Pattern

When accessing observable object properties, MobX records the access path using the visitor pattern, creating a dependency tree that tracks which computed values or reactions depend on which observables.

class ObservableObject {
  @observable prop;
  // ...
}

const obj = new ObservableObject();
autorun(() => {
  console.log(obj.prop);
});

// Accessing obj.prop records the dependency
obj.prop = "new value";

Change Notifications

When an observable state changes, MobX notifies all dependent computed values and reaction functions. Since dependencies are tracked, only affected computations and reactions are triggered.

Minimized Updates

Dependency tracking ensures only necessary computed values and reactions are executed, improving performance by avoiding redundant calculations.

Optimizations

MobX provides optimization mechanisms, such as using asFlat, asReference, or asStructure methods to control how proxies handle changes, further enhancing performance.

Middleware Integration

While MobX is not as tightly integrated with middleware as Redux, you can use mobx-react-devtools to monitor state changes, offering features like time-travel debugging.

Installing the Plugin

Install mobx-react-devtools using npm or yarn:

npm install --save mobx-react-devtools
# or
yarn add mobx-react-devtools

Integrating into Your Application

In your main application file (typically index.js or App.js), import and include the mobxReactDevTools component:

import { Provider } from 'mobx-react';
import { mobxReactDevTools } from 'mobx-react-devtools';

// Assuming you have your store
const store = new YourStore();

ReactDOM.render(
  <Provider {...store}>
    <YourApp />
    {/* Add DevTools below your app */}
    <mobxReactDevTools />
  </Provider>,
  document.getElementById('root')
);

Enabling Developer Tools

Once your application is running, a new panel in the browser’s developer tools displays your MobX state and change history. In Chrome or Firefox, find it under the “.mobx-react-devtools” or “Extensions” panel.

Time Travel Debugging

While mobx-react-devtools does not directly support time-travel debugging, you can use mobx-state-tree, a MobX-compatible library that provides time-travel functionality by creating a reversible operation history.

TypeScript Support

MobX integrates well with TypeScript, offering type safety and improved code hints.

Type Annotations

In TypeScript, add type annotations to observables, computed values, and actions for type safety.

import { observable, computed, action } from 'mobx';

class Counter {
  @observable count: number = 0;

  @computed
  get isEven(): boolean {
    return this.count % 2 === 0;
  }

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

Interfaces

Define interfaces for complex observable object structures to ensure data model consistency.

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

class TodoStore {
  @observable todos: Todo[] = [];

  // ...
}

Type Inference

TypeScript automatically infers computed value types based on their dependencies.

Type Guards

Use type guard functions to ensure type-safe access to observable objects.

function isTodo(item: any): item is Todo {
  return item && typeof item.id === 'number' && typeof item.title === 'string';
}

const todo = getTodoFromSomewhere();

if (isTodo(todo)) {
  // TypeScript knows `todo` is of type `Todo`
  console.log(todo.title);
}

Type Extensions

Extend ObservableObject, ObservableArray, and ObservableMap types to better describe your data.

makeObservable and makeAutoObservable

In MobX 6, use makeObservable and makeAutoObservable for initializing observable state, providing better type safety and automatic type inference.

class TodoStore {
  private todos: Todo[] = [];

  constructor() {
    makeAutoObservable(this);
  }

  // ...
}

Reactive Data Flow

MobX’s reactive data flow ensures data changes automatically propagate to dependent computations and views, clarifying the relationship between data models and UI.

import React from 'react';
import { render } from 'react-dom';
import { observable, computed, action, reaction } from 'mobx';
import { Provider, observer } from 'mobx-react';

// Create observable object
const counterStore = {
  @observable count: 0,

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

  @computed get doubleCount() {
    return this.count * 2;
  },
};

// Create a reaction to print doubleCount when count changes
reaction(
  () => counterStore.count,
  (newCount) => {
    console.log(`Double count: ${counterStore.doubleCount}`);
  },
);

// Create a React component to observe count changes
const Counter = observer(({ store }) => (
  <div>
    <p>Count: {store.count}</p>
    <button onClick={() => store.increment()}>Increment</button>
    <p>Double Count: {store.doubleCount}</p>
  </div>
));

// Render the application
render(
  <Provider counterStore={counterStore}>
    <Counter />
  </Provider>,
  document.getElementById('root'),
);

In this example, counterStore is an observable object with a count property. doubleCount is a computed value that calculates based on count. When count increments, both doubleCount and the Counter component update automatically without manual setState calls.

The reaction function creates an observer that prints doubleCount when count changes, forming a clear reactive data flow.

Reactive Functions

Using autorun, reaction, or when functions, you can create functions that execute automatically based on data changes, running until a condition is met or manually stopped.

const disposer = reaction(
  () => todoList.completedCount,
  (completedCount) => console.log(`There are ${completedCount} completed todos`),
);

// Later, to stop the reaction:
disposer();

Responsive API Calls

For API calls with MobX, wrap asynchronous operations in runInAction to ensure state updates occur in the correct scope.

async function fetchData() {
  await someAsyncCall().then(data => {
    runInAction(() => {
      // Update state
      myStore.setData(data);
    });
  });
}

Microkernel Architecture

MobX’s core is lightweight, allowing selective inclusion of additional features like mobx-state-tree or mobx-react-form for enhanced state management and form handling.

mobx-state-tree

mobx-state-tree is a MobX-based state management library offering strong type safety, state snapshots, time-travel debugging, and robust error handling.

import { types, onSnapshot } from 'mobx-state-tree';

const Todo = types.model({
  title: types.string,
  completed: types.boolean,
});

const TodoStore = types.model({
  todos: types.array(Todo),
}).actions(self => ({
  addTodo(title: string) {
    self.todos.push(Todo.create({ title, completed: false }));
  },
}));

const store = TodoStore.create({ todos: [] });

onSnapshot(store, (snapshot) => {
  console.log('State snapshot:', snapshot);
});

store.addTodo('Learn MobX');

mobx-react-form

mobx-react-form is a library for creating and managing forms, integrating seamlessly with MobX and providing validation, submission, and reset functionalities.

import React from 'react';
import { Form, Field } from 'mobx-react-form';
import { observable, action } from 'mobx';
import { Provider, observer } from 'mobx-react';

const schema = {
  name: 'form',
  fields: ['name', 'email'],
  labels: { name: 'Name', email: 'Email' },
  validators: {
    name: 'required',
    email: 'required|email',
  },
};

class MyForm extends Form {
  constructor(values = {}) {
    super(schema, values);
    this.plugins({
      dvr: {},
    });
  }
}

const form = new MyForm();

@observer
class MyComponent extends React.Component {
  @observable submitting = false;

  @action submitForm = () => {
    this.submitting = true;
    if (form.validate()) {
      alert('Form submitted successfully!');
      form.reset();
    } else {
      form.focus();
    }
    this.submitting = false;
  };

  render() {
    return (
      <Provider form={form}>
        <form onSubmit={this.submitForm}>
          <Field type="text" name="name" />
          <Field type="email" name="email" />
          <button type="submit" disabled={this.submitting}>
            Submit
          </button>
        </form>
      </Provider>
    );
  }
}

render(<MyComponent />, document.getElementById('root'));

These libraries extend MobX’s core functionality, providing advanced abstractions for state management and form handling. This microkernel architecture allows you to choose tools based on project needs, keeping the project lightweight and modular.

Integration with Other Libraries

MobX is not limited to React; it integrates well with Vue.js, Angular, and other libraries. It can also coexist with Redux or other state management libraries for specific use cases.

Hot Reloading and Development Tools

MobX, combined with the mobx-react-devtools plugin, provides capabilities for inspecting data flow, tracking dependencies, and analyzing performance during development, supporting hot reloading for rapid iteration.

Performance Optimization

MobX’s reactive system automatically tracks dependencies, updating views only when necessary, which is typically more efficient than manual updates. For performance issues, use makeObservable or makeAutoObservable with asStructure or asReference options, and trackTransitions to fine-tune performance.

Design Patterns

MobX encourages design patterns like MVC (Model-View-Controller), MVVM (Model-View-ViewModel), or MST (MobX State Tree) to better organize and manage state.

Web Development

Part 20 of 46

The content covers the three basics of HTML/CSS/JS/TS, modular development, mainstream frameworks (Vue, React, Angular, Svelte), build tools, browser plug-in development, Node.js backend practice, Next.js/Nest.js and other popular technologies.

Up next

Redux Toolkit-Simplifying Redux Application State Management

Redux Toolkit is the officially recommended toolset for simplifying Redux development. It incorporates preset best practices, making it easier to create and manage Redux state. 1. Creating a Store Use the configureStore function to create a Redux sto...

More from this blog

T

Tianya School Technical Articles

48 posts

Welcome to our tech publication, where we share high-quality content on full-stack development, frontend/backend/web3/AI, and engineering best practices.