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

MobX Паттерны и best practices

Две стратегии для асинхронных actions:

import { makeAutoObservable, runInAction } from 'mobx';
class ProductStore {
products: Product[] = [];
isLoading = false;
constructor() { makeAutoObservable(this); }
async loadProducts() {
this.isLoading = true;
try {
const data = await productApi.getAll();
runInAction(() => {
this.products = data;
this.isLoading = false;
});
} catch (err) {
runInAction(() => { this.isLoading = false; });
throw err;
}
}
}

flow автоматически оборачивает каждый шаг после await в action:

import { makeObservable, observable, action, flow } from 'mobx';
class ProductStore {
products: Product[] = [];
isLoading = false;
constructor() {
makeObservable(this, {
products: observable,
isLoading: observable,
loadProducts: flow, // генератор — объявляем как flow
});
}
// Генератор вместо async function
*loadProducts() {
this.isLoading = true;
try {
// yield вместо await
const data: Product[] = yield productApi.getAll();
this.products = data; // ✅ не нужен runInAction!
this.isLoading = false;
} catch (err) {
this.isLoading = false;
throw err;
}
}
}
// С makeAutoObservable — использовать flowResult для типизации
import { makeAutoObservable, flow, flowResult } from 'mobx';
class Store {
data: string[] = [];
constructor() { makeAutoObservable(this, { fetchData: flow }); }
*fetchData(query: string) {
const result: string[] = yield fetch(`/api?q=${query}`).then(r => r.json());
this.data = result;
return result;
}
}
// Использование с типизацией
const store = new Store();
const result = await flowResult(store.fetchData('react'));
class SearchStore {
results: string[] = [];
query = '';
private abortController: AbortController | null = null;
constructor() { makeAutoObservable(this, { search: flow }); }
setQuery(query: string) {
this.query = query;
// Отменяем предыдущий запрос
this.abortController?.abort();
}
*search() {
this.abortController = new AbortController();
try {
const data: string[] = yield fetch(
`/api/search?q=${this.query}`,
{ signal: this.abortController.signal }
).then(r => r.json());
this.results = data;
} catch (err) {
if ((err as Error).name !== 'AbortError') throw err;
}
}
}
import { reaction, autorun } from 'mobx';
import { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
// Выполнить side-effect при изменении observable (логирование, аналитика, API)
const UserForm = observer(function UserForm() {
const store = useUserStore();
// reaction в useEffect — правильная интеграция
useEffect(() => {
const disposer = reaction(
() => store.currentUser?.id,
(userId) => {
if (userId) {
analytics.identify(userId);
}
}
);
return disposer; // cleanup при размонтировании
}, [store]);
return <form>...</form>;
});
// Синхронизация MobX state → URL
function useUrlSync(store: FilterStore) {
useEffect(() => {
const disposer = autorun(() => {
const params = new URLSearchParams();
if (store.query) params.set('q', store.query);
if (store.page > 1) params.set('page', String(store.page));
window.history.replaceState(null, '', `?${params}`);
});
return disposer;
}, [store]);
}
configure({ enforceActions: 'always' });
class Store {
count = 0;
constructor() { makeAutoObservable(this); }
}
const store = new Store();
// ❌ Ошибка: изменение вне action
setTimeout(() => {
store.count++; // MobX: [mobx] Since strict-mode is enabled, changing observed values...
}, 1000);
// ✅ Правильно
class Store2 {
count = 0;
constructor() { makeAutoObservable(this); }
increment() { this.count++; } // action
}
setTimeout(() => store2.increment(), 1000);
class BadStore {
users: User[] = [];
constructor() { makeAutoObservable(this); }
// ❌ computed не должен иметь side effects!
get userCount() {
console.log('calculating...'); // плохо — логирование
fetch('/api/log'); // ОЧЕНЬ плохо — запрос в computed!
return this.users.length;
}
}
// ✅ computed — только чистые вычисления
class GoodStore {
users: User[] = [];
constructor() { makeAutoObservable(this); }
get userCount() {
return this.users.length; // только вычисление
}
}
interface CardProps {
user: User; // User — MobX observable object
}
// ❌ Не observer — user.name может обновиться, но компонент не перерисуется
function UserCard({ user }: CardProps) {
return <div>{user.name}</div>;
}
// ✅ Обернуть в observer
const UserCard = observer(function UserCard({ user }: CardProps) {
return <div>{user.name}</div>;
});

Ошибка 4: Создание Store в компоненте без useState

Заголовок раздела «Ошибка 4: Создание Store в компоненте без useState»
// ❌ Новый store при каждом рендере!
function BadComponent() {
const store = new ItemStore(); // пересоздаётся при каждом рендере
return <observer>...</observer>;
}
// ✅ useState для стабильной ссылки
function GoodComponent() {
const [store] = useState(() => new ItemStore());
return <observer>...</observer>;
}
// 1. Один store — одна зона ответственности
class AuthStore { /* только авторизация */ }
class CartStore { /* только корзина */ }
class UIStore { /* только UI состояние (modals, toasts) */ }
// 2. Держи производное состояние в computed, не в state
class BadStore {
users: User[] = [];
adminCount = 0; // ❌ избыточно — выводится из users
addUser(user: User) {
this.users.push(user);
if (user.role === 'admin') this.adminCount++; // синхронизировать вручную
}
}
class GoodStore {
users: User[] = [];
constructor() { makeAutoObservable(this); }
get adminCount() { // ✅ computed — всегда актуально
return this.users.filter(u => u.role === 'admin').length;
}
}
// 3. toJS для serialization / передачи в non-observable контекст
import { toJS } from 'mobx';
function serializeStore(store: UserStore) {
return JSON.stringify(toJS(store.users));
// Без toJS JSON.stringify может не обойти Proxy MobX
}