63. MobX: Продвинутый уровень
MobX: Продвинутый уровень — Async, Flow и Архитектура 🏗️
Заголовок раздела «MobX: Продвинутый уровень — Async, Flow и Архитектура 🏗️»Ты освоил основы MobX. Теперь — продвинутые техники: как правильно делать асинхронные операции, как структурировать большие приложения и использовать мощные инструменты MobX. Добро пожаловать на следующий уровень! 🚀
1. Проблема асинхронных actions ⚠️
Заголовок раздела «1. Проблема асинхронных actions ⚠️»MobX требует, чтобы изменения observable происходили внутри action. С async функциями это создаёт проблему — код после await выполняется вне контекста оригинального action:
import { makeAutoObservable } from 'mobx';
class UserStore { users: User[] = []; isLoading = false;
constructor() { makeAutoObservable(this); }
// ❌ Проблема: изменения после await — вне action! async fetchUsersBad() { this.isLoading = true; // ✅ внутри action const data = await api.getUsers(); // 💥 Всё что ниже — уже НЕ в action! this.users = data; // ⚠️ предупреждение MobX в strict mode this.isLoading = false; // ⚠️ предупреждение MobX в strict mode }}2. runInAction() — Быстрое решение 🏃
Заголовок раздела «2. runInAction() — Быстрое решение 🏃»runInAction() позволяет обернуть изменения в action прямо внутри async функции:
import { makeAutoObservable, runInAction } from 'mobx';
class UserStore { users: User[] = []; isLoading = false; error: string | null = null;
constructor() { makeAutoObservable(this); }
async fetchUsers() { this.isLoading = true; this.error = null;
try { const data = await api.getUsers();
// ✅ Все изменения ПОСЛЕ await — в runInAction runInAction(() => { this.users = data; this.isLoading = false; }); } catch (err) { runInAction(() => { this.error = err.message; this.isLoading = false; }); } }}3. flow() — Элегантное решение с генераторами 🌊
Заголовок раздела «3. flow() — Элегантное решение с генераторами 🌊»flow() — это рекомендованный MobX способ для async actions. Использует генераторы вместо async/await. MobX автоматически оборачивает каждую часть кода после yield в action:
import { makeAutoObservable, flow } from 'mobx';
class UserStore { users: User[] = []; isLoading = false; error: string | null = null;
constructor() { makeAutoObservable(this, { fetchUsers: flow, // явно указываем что метод — flow }); }
// flow-генератор: yield вместо await *fetchUsers() { this.isLoading = true; this.error = null;
try { // yield работает как await, но MobX знает про контекст const data: User[] = yield api.getUsers(); this.users = data; // ✅ автоматически в action! this.isLoading = false; // ✅ автоматически в action! } catch (err: any) { this.error = err.message; this.isLoading = false; } }}Преимущества flow():
- ✅ Автоматический action-контекст после каждого
yield - ✅ Можно отменить (
flowInstance.cancel()) - ✅ Лучший TypeScript-опыт с
flowResult() - ✅ Рекомендован документацией MobX
4. Отмена flow — важная возможность 🚫
Заголовок раздела «4. Отмена flow — важная возможность 🚫»import { flow, makeAutoObservable } from 'mobx';
class SearchStore { results: string[] = []; isSearching = false; private currentSearch: ReturnType<typeof this.search> | null = null;
constructor() { makeAutoObservable(this, { search: flow }); }
*search(query: string) { this.isSearching = true; try { yield new Promise(r => setTimeout(r, 300)); // debounce const results: string[] = yield api.search(query); this.results = results; } finally { this.isSearching = false; } }
startSearch(query: string) { // Отменяем предыдущий поиск if (this.currentSearch) { this.currentSearch.cancel(); } this.currentSearch = this.search(query); }}5. Interceptors и Spy — отладка MobX 🔍
Заголовок раздела «5. Interceptors и Spy — отладка MobX 🔍»import { observe, intercept, spy } from 'mobx';
// observe: следим за изменениями конкретного observableconst disposer = observe(store, 'count', (change) => { console.log(`count: ${change.oldValue} → ${change.newValue}`);});
// intercept: перехватываем И можем отменить изменениеintercept(store, 'age', (change) => { if (change.newValue < 0) { console.warn('Возраст не может быть отрицательным!'); return null; // отменяем изменение! } return change; // разрешаем изменение});
// spy: глобальный наблюдатель за ВСЕМИ событиями MobXconst stopSpy = spy((event) => { if (event.type === 'action') { console.log(`Action: ${event.name}`); }});// Используй spy только для отладки, убирай в продакшене!6. Root Store Pattern — структура большого приложения 🏛️
Заголовок раздела «6. Root Store Pattern — структура большого приложения 🏛️»Когда приложение растёт, нужна четкая структура. Паттерн Root Store объединяет все сторы:
class UserStore { users: User[] = []; currentUser: User | null = null;
constructor(private root: RootStore) { makeAutoObservable(this); }
get isLoggedIn() { return this.currentUser !== null; }
async login(email: string, password: string) { const user = await authApi.login(email, password); runInAction(() => { this.currentUser = user; this.root.cartStore.loadForUser(user.id); // доступ к другому стору! }); }}
// stores/CartStore.tsclass CartStore { items: CartItem[] = [];
constructor(private root: RootStore) { makeAutoObservable(this); }
async loadForUser(userId: string) { const items = await cartApi.getCart(userId); runInAction(() => { this.items = items; }); }}
// stores/RootStore.ts — главный сторclass RootStore { userStore: UserStore; cartStore: CartStore; uiStore: UIStore;
constructor() { this.userStore = new UserStore(this); this.cartStore = new CartStore(this); this.uiStore = new UIStore(this); }}
export const rootStore = new RootStore();7. Persistence — сохранение стейта 💾
Заголовок раздела «7. Persistence — сохранение стейта 💾»import { autorun, toJS } from 'mobx';
class PersistentStore { theme: 'light' | 'dark' = 'dark'; language = 'ru'; favorites: string[] = [];
constructor() { makeAutoObservable(this); this.hydrate(); // загрузка из localStorage this.setupPersistence(); // автосохранение }
hydrate() { const saved = localStorage.getItem('app-store'); if (saved) { const data = JSON.parse(saved); this.theme = data.theme ?? 'dark'; this.language = data.language ?? 'ru'; this.favorites = data.favorites ?? []; } }
setupPersistence() { // Автосохранение при любом изменении autorun(() => { localStorage.setItem('app-store', JSON.stringify({ theme: this.theme, language: this.language, favorites: toJS(this.favorites), // toJS: MobX array → plain array })); }); }}