61. MobX: Computed и Reactions
MobX: Computed и Reactions — Магия реактивности ⚡
Заголовок раздела «MobX: Computed и Reactions — Магия реактивности ⚡»Если Observable — это данные, то Computed и Reactions — это то, как MobX автоматически реагирует на их изменения. Это как нейронная сеть: поменялся один нейрон — автоматически обновились все связанные! 🧠
1. computed() — Производные значения 🧮
Заголовок раздела «1. computed() — Производные значения 🧮»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
2. Computed vs Обычный метод 🆚
Заголовок раздела «2. Computed vs Обычный метод 🆚»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(); // → "Метод вызван!" + 15store.getSumMethod(); // → "Метод вызван!" + 15 (снова!)store.getSumMethod(); // → "Метод вызван!" + 15 (и снова!)
// Computed кэшируетсяstore.sumComputed; // → "Computed пересчитан!" + 15store.sumComputed; // → 15 (из кэша, без вычислений!)store.sumComputed; // → 15 (из кэша!)3. autorun() — Автоматический запуск 🤖
Заголовок раздела «3. autorun() — Автоматический запуск 🤖»autorun — это реакция, которая:
- Запускается сразу при создании
- Повторно запускается каждый раз, когда изменяются её observable-зависимости
import { observable, autorun } from 'mobx';
const store = observable({ count: 0, name: 'Яша' });
// Сразу выводит: "Яша: 0"// Потом запускается при каждом изменении count или nameconst 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 с состоянием
- Дебаг-инструменты
// Автосохранение в localStorageautorun(() => { localStorage.setItem('cart', JSON.stringify(cartStore.items));});
// Синхронизация URLautorun(() => { window.history.pushState({}, '', `?page=${routerStore.currentPage}`);});4. reaction() — Умная реакция 🎯
Заголовок раздела «4. reaction() — Умная реакция 🎯»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 секунду после последнего изменения);5. when() — Одноразовая реакция ⏰
Заголовок раздела «5. when() — Одноразовая реакция ⏰»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+ товаров в корзине! 🛒'));6. Сравнение реакций 📊
Заголовок раздела «6. Сравнение реакций 📊»autorun | reaction | when | |
|---|---|---|---|
| Запуск при создании | ✅ Да | ❌ Нет | ❌ Нет |
| Получает старое значение | ❌ | ✅ | ❌ |
| Количество запусков | ∞ | ∞ | 1 |
| Условие остановки | Вручную | Вручную | Автоматически |
| Когда использовать | Логи, синхронизация | API-запросы, аналитика | Инициализация |
7. Очистка реакций — важно! 🧹
Заголовок раздела «7. Очистка реакций — важно! 🧹»Всегда сохраняй функцию очистки, чтобы избежать утечек памяти:
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()); }}