Концепции MobX 6
Философия MobX
Заголовок раздела «Философия MobX»MobX работает по принципу «всё что можно вывести из состояния — должно выводиться автоматически». В отличие от Redux, где изменения проходят через reducer, MobX отслеживает зависимости автоматически.
Ключевые понятия:
observable— реактивное состояниеcomputed— производное состояние (автоматически пересчитывается)action— функция изменяющая состояниеautorun/reaction/when— реакции на изменения
observable
Заголовок раздела «observable»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; }}observable.box, observable.map, observable.set
Заголовок раздела «observable.box, observable.map, observable.set»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
// Setconst tags = observable.set<string>(['react', 'typescript']);tags.add('mobx');computed
Заголовок раздела «computed»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); // 0cart.addItem({ name: 'React книга', price: 1500 });console.log(cart.total); // 1500Actions — единственный способ изменять 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; }); } }}autorun, reaction, when
Заголовок раздела «autorun, reaction, when»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 как Promiseawait when(() => store.count > 10);console.log('Наконец-то > 10');
// Всегда удаляй подписки!disposer1();disposer2();Strict mode и configure
Заголовок раздела «Strict mode и configure»import { configure } from 'mobx';
// Обязательное использование actions для изменения состоянияconfigure({ enforceActions: 'always', // или 'observed', 'never' computedRequiresReaction: true, reactionRequiresObservable: true, observableRequiresReaction: true, // предупреждение если observable читается вне реакции});