DEV Community

Tianya School
Tianya School

Posted on

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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++;
Enter fullscreen mode Exit fullscreen mode

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++;
Enter fullscreen mode Exit fullscreen mode

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!');
  },
);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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++;
  }
}
Enter fullscreen mode Exit fullscreen mode

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[] = [];

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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'),
);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)