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

61. MobX: Computed и Reactions

Если Observable — это данные, то Computed и Reactions — это то, как MobX автоматически реагирует на их изменения. Это как нейронная сеть: поменялся один нейрон — автоматически обновились все связанные! 🧠


Computed value — это значение, которое выводится из других observable. Как формула в Excel: =A1+B1. Меняется A1 — автоматически пересчитывается C1.

import { makeAutoObservable, computed } from 'mobx';
class CartStore {
items: { name: string; price: number; qty: number }[] = [];
taxRate = 0.2; // 20% НДС
constructor() {
makeAutoObservable(this);
}
// computed: пересчитывается только когда items или taxRate изменятся
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
get tax() {
return this.subtotal * this.taxRate; // зависит от subtotal (другой computed!)
}
get total() {
return this.subtotal + this.tax; // зависит от subtotal и tax
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.qty, 0);
}
}

Ключевые свойства computed:

  • 🚀 Кэширование: результат кэшируется, повторный вызов не пересчитывает
  • 🎯 Ленивость: пересчитывается только когда кто-то читает значение
  • 🔄 Цепочки: computed может зависеть от другого computed

class Store {
items = [1, 2, 3, 4, 5];
// ❌ Метод: вычисляется КАЖДЫЙ РАЗ при вызове
getSumMethod() {
console.log('Метод вызван!');
return this.items.reduce((a, b) => a + b, 0);
}
// ✅ Computed: кэшируется, пересчитывается ТОЛЬКО при изменении items
get sumComputed() {
console.log('Computed пересчитан!');
return this.items.reduce((a, b) => a + b, 0);
}
}
const store = new Store();
// Метод вычисляется каждый раз
store.getSumMethod(); // → "Метод вызван!" + 15
store.getSumMethod(); // → "Метод вызван!" + 15 (снова!)
store.getSumMethod(); // → "Метод вызван!" + 15 (и снова!)
// Computed кэшируется
store.sumComputed; // → "Computed пересчитан!" + 15
store.sumComputed; // → 15 (из кэша, без вычислений!)
store.sumComputed; // → 15 (из кэша!)

autorun — это реакция, которая:

  1. Запускается сразу при создании
  2. Повторно запускается каждый раз, когда изменяются её observable-зависимости
import { observable, autorun } from 'mobx';
const store = observable({ count: 0, name: 'Яша' });
// Сразу выводит: "Яша: 0"
// Потом запускается при каждом изменении count или name
const stop = autorun(() => {
console.log(`${store.name}: ${store.count}`);
});
store.count = 1; // → "Яша: 1"
store.count = 2; // → "Яша: 2"
store.name = 'Петя'; // → "Петя: 2"
// Остановить реакцию (очистить)
stop();
store.count = 3; // тишина — autorun уже не работает

Частые применения autorun:

  • Логирование изменений стейта
  • Сохранение в localStorage при каждом изменении
  • Синхронизация URL с состоянием
  • Дебаг-инструменты
// Автосохранение в localStorage
autorun(() => {
localStorage.setItem('cart', JSON.stringify(cartStore.items));
});
// Синхронизация URL
autorun(() => {
window.history.pushState({}, '', `?page=${routerStore.currentPage}`);
});

reaction мощнее autorun: ты сам указываешь что отслеживать и что делать при изменении.

import { reaction } from 'mobx';
reaction(
() => store.count, // 1) функция-наблюдатель: ЧТО отслеживать
(newValue, prevValue) => { // 2) эффект: ЧТО делать при изменении
console.log(`Было: ${prevValue}, Стало: ${newValue}`);
}
);
// Отличия от autorun:
// 1. НЕ запускается сразу (только при первом изменении)
// 2. Получает предыдущее И новое значение
// 3. Зависимость определяется первой функцией, эффект — второй

Практический пример — отправка на сервер только при изменении:

reaction(
() => userStore.profile, // отслеживаем профиль
async (profile) => {
// Отправляем на сервер только при реальном изменении
await api.updateProfile(profile);
console.log('Профиль сохранён!');
},
{ delay: 1000 } // debounce: ждём 1 секунду после последнего изменения
);

when ждёт пока условие станет true и выполняет действие один раз:

import { when } from 'mobx';
// Вариант 1: с колбэком
when(
() => userStore.isLoggedIn, // условие
() => console.log('Пользователь вошёл!') // действие (один раз!)
);
// Вариант 2: как промис (await!)
async function waitForUser() {
await when(() => userStore.isLoggedIn);
console.log('Теперь можем грузить данные');
await dataStore.loadUserData();
}
// Пример: показать уведомление когда корзина заполнится
when(
() => cartStore.itemCount >= 5,
() => notificationStore.show('У вас 5+ товаров в корзине! 🛒')
);

autorunreactionwhen
Запуск при создании✅ Да❌ Нет❌ Нет
Получает старое значение
Количество запусков1
Условие остановкиВручнуюВручнуюАвтоматически
Когда использоватьЛоги, синхронизацияAPI-запросы, аналитикаИнициализация

Всегда сохраняй функцию очистки, чтобы избежать утечек памяти:

class ComponentStore {
private disposers: (() => void)[] = [];
constructor() {
// Сохраняем disposer
this.disposers.push(
autorun(() => {
document.title = `${appStore.notifications.length} уведомлений`;
})
);
this.disposers.push(
reaction(
() => authStore.isLoggedIn,
(isLoggedIn) => isLoggedIn ? this.onLogin() : this.onLogout()
)
);
}
// Вызывать при размонтировании компонента
dispose() {
this.disposers.forEach(d => d());
}
}