Перейти к содержимому

Концепции MobX 6

MobX работает по принципу «всё что можно вывести из состояния — должно выводиться автоматически». В отличие от Redux, где изменения проходят через reducer, MobX отслеживает зависимости автоматически.

Ключевые понятия:

  • observable — реактивное состояние
  • computed — производное состояние (автоматически пересчитывается)
  • action — функция изменяющая состояние
  • autorun / reaction / when — реакции на изменения
import { observable, makeObservable, makeAutoObservable } from 'mobx';
// Вариант 1: makeObservable — явное объявление
class UserStore {
name = 'Иван';
age = 25;
friends: string[] = [];
constructor() {
makeObservable(this, {
name: observable,
age: observable,
friends: observable,
fullName: computed,
setName: action,
});
}
get fullName() {
return `${this.name}, ${this.age} лет`;
}
setName(name: string) {
this.name = name;
}
}
// Вариант 2: makeAutoObservable — автоматическое определение
class UserStore2 {
name = 'Иван';
age = 25;
friends: string[] = [];
constructor() {
makeAutoObservable(this);
// Правила: геттеры → computed, методы → action, поля → observable
}
get fullName() {
return `${this.name}, ${this.age} лет`;
}
setName(name: string) {
this.name = name;
}
}
import { observable } from 'mobx';
// Для примитивов вне класса
const count = observable.box(0);
count.set(1);
console.log(count.get()); // 1
// Map с реактивными ключами и значениями
const userMap = observable.map<number, string>();
userMap.set(1, 'Иван');
userMap.has(1); // true
// Set
const tags = observable.set<string>(['react', 'typescript']);
tags.add('mobx');

Computed значения кэшируются и пересчитываются только при изменении зависимостей:

import { makeAutoObservable, computed } from 'mobx';
class ShoppingCart {
items: Array<{ name: string; price: number; qty: number }> = [];
discountPercent = 0;
constructor() {
makeAutoObservable(this);
}
// computed — пересчитывается только когда items меняется
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
// computed зависит от другого computed
get total() {
return this.subtotal * (1 - this.discountPercent / 100);
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.qty, 0);
}
addItem(item: { name: string; price: number }) {
const existing = this.items.find(i => i.name === item.name);
if (existing) {
existing.qty++;
} else {
this.items.push({ ...item, qty: 1 });
}
}
}
const cart = new ShoppingCart();
console.log(cart.total); // 0
cart.addItem({ name: 'React книга', price: 1500 });
console.log(cart.total); // 1500

Actions — единственный способ изменять observable состояние в strict mode:

import { makeAutoObservable, action, runInAction } from 'mobx';
class TodoStore {
todos: Todo[] = [];
isLoading = false;
error: string | null = null;
constructor() {
makeAutoObservable(this);
}
// Синхронный action
addTodo(text: string) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
// Асинхронный action — нельзя просто await, нужно runInAction
async fetchTodos() {
this.isLoading = true;
this.error = null;
try {
const data = await fetch('/api/todos').then(r => r.json());
runInAction(() => {
// Изменения состояния после await — только в runInAction!
this.todos = data;
this.isLoading = false;
});
} catch (err) {
runInAction(() => {
this.error = String(err);
this.isLoading = false;
});
}
}
}
import { autorun, reaction, when, observable } from 'mobx';
class Store {
count = 0;
constructor() { makeAutoObservable(this); }
increment() { this.count++; }
}
const store = new Store();
// autorun: запускается немедленно и при каждом изменении зависимостей
const disposer1 = autorun(() => {
console.log('count:', store.count); // вызывается сразу (0), потом при изменении
});
// reaction: явно разделяет "что отслеживать" и "что делать"
// Не запускается сразу (в отличие от autorun)
const disposer2 = reaction(
() => store.count, // отслеживаем
(count, prevCount) => { // реагируем
console.log(`${prevCount}${count}`);
}
);
// when: одноразовый — срабатывает когда условие true, потом отписывается
const disposer3 = when(
() => store.count > 5,
() => console.log('count превысил 5!')
);
// when как Promise
await when(() => store.count > 10);
console.log('Наконец-то > 10');
// Всегда удаляй подписки!
disposer1();
disposer2();
import { configure } from 'mobx';
// Обязательное использование actions для изменения состояния
configure({
enforceActions: 'always', // или 'observed', 'never'
computedRequiresReaction: true,
reactionRequiresObservable: true,
observableRequiresReaction: true, // предупреждение если observable читается вне реакции
});