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

63. MobX: Продвинутый уровень

MobX: Продвинутый уровень — Async, Flow и Архитектура 🏗️

Заголовок раздела «MobX: Продвинутый уровень — Async, Flow и Архитектура 🏗️»

Ты освоил основы MobX. Теперь — продвинутые техники: как правильно делать асинхронные операции, как структурировать большие приложения и использовать мощные инструменты MobX. Добро пожаловать на следующий уровень! 🚀


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
}
}

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

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);
}
}

import { observe, intercept, spy } from 'mobx';
// observe: следим за изменениями конкретного observable
const 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: глобальный наблюдатель за ВСЕМИ событиями MobX
const stopSpy = spy((event) => {
if (event.type === 'action') {
console.log(`Action: ${event.name}`);
}
});
// Используй spy только для отладки, убирай в продакшене!

6. Root Store Pattern — структура большого приложения 🏛️

Заголовок раздела «6. Root Store Pattern — структура большого приложения 🏛️»

Когда приложение растёт, нужна четкая структура. Паттерн Root Store объединяет все сторы:

stores/UserStore.ts
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.ts
class 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();

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
}));
});
}
}