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

62. MobX + React

MobX и React — это идеальный союз. MobX управляет состоянием, React рисует UI. Связующее звено — observer() из библиотеки mobx-react-lite. Как пульт управления телевизором: нажал кнопку (action) — телевизор (компонент) обновился! 📺


1. observer() — Делаем компонент реактивным 👁️

Заголовок раздела «1. observer() — Делаем компонент реактивным 👁️»

observer() — это HOC (Higher-Order Component), который оборачивает компонент и делает его реактивным. Компонент автоматически перерендерится когда изменяются observable-данные, которые он читает.

import { observer } from 'mobx-react-lite';
import { counterStore } from './stores';
// Без observer — не будет реагировать на изменения MobX
function BadCounter() {
return <div>{counterStore.count}</div>; // ❌ не обновится!
}
// С observer — автоматически перерендерится при изменении count
const GoodCounter = observer(() => {
return <div>{counterStore.count}</div>; // ✅ работает!
});
// Или для именованного компонента
const Counter = observer(function Counter() {
return (
<button onClick={() => counterStore.increment()}>
Клики: {counterStore.count}
</button>
);
});

observer() оборачивает рендер-функцию компонента в специальный tracking context. Во время рендера MobX запоминает все observable-значения, которые были прочитаны. Если любое из них изменится — компонент перерендерится.

const ProductCard = observer(({ productId }: { productId: number }) => {
// MobX запоминает что этот компонент читает:
// - products.find() — следит за массивом products
// - product.name, product.price, product.inStock — следит за этими полями
const product = productsStore.products.find(p => p.id === productId);
if (!product) return null;
return (
<div>
<h3>{product.name}</h3> {/* отслеживается */}
<p>{product.price} ₽</p> {/* отслеживается */}
{product.inStock && <span>В наличии</span>} {/* отслеживается */}
</div>
);
// Важно: изменение product.description НЕ вызовет рендер,
// если мы его не читаем в этом компоненте!
});

3. useLocalObservable() — Локальный стейт в компоненте 🏠

Заголовок раздела «3. useLocalObservable() — Локальный стейт в компоненте 🏠»

Когда стейт нужен только в одном компоненте и не стоит выносить его в глобальный стор:

import { useLocalObservable } from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
const SearchBox = observer(() => {
// Создаёт observable объект, живущий всё время монтирования компонента
const state = useLocalObservable(() => ({
query: '',
results: [] as string[],
isLoading: false,
get hasResults() {
return this.results.length > 0;
},
setQuery(q: string) {
this.query = q;
},
async search() {
this.isLoading = true;
// имитация поиска
await new Promise(r => setTimeout(r, 500));
this.results = ['Результат 1', 'Результат 2'];
this.isLoading = false;
},
}));
return (
<div>
<input
value={state.query}
onChange={e => state.setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && state.search()}
/>
{state.isLoading && <span>Загрузка...</span>}
{state.hasResults && state.results.map(r => <div key={r}>{r}</div>)}
</div>
);
});

4. Правила MobX + React: как избежать проблем ⚠️

Заголовок раздела «4. Правила MobX + React: как избежать проблем ⚠️»
// ❌ Плохо: только родитель — observer, дети нет
const Parent = observer(() => {
return <Child name={store.user.name} />; // передаём значение
});
// child НЕ обновится при изменении store.user.age!
const Child = ({ name }: { name: string }) => <div>{name}</div>;
// ✅ Хорошо: дети тоже observer, и они сами читают из стора
const Parent = observer(() => {
return <Child />;
});
const Child = observer(() => {
return <div>{store.user.name} ({store.user.age})</div>; // ✅
});

Правило 2: Не деструктурируй observable вне observer! 📦

Заголовок раздела «Правило 2: Не деструктурируй observable вне observer! 📦»
// ❌ Плохо: деструктуризация разрывает связь с observable
const { name, age } = store.user; // MobX теряет трекинг!
const Broken = () => <div>{name} {age}</div>; // не обновится!
// ✅ Хорошо: читаем свойства прямо в render
const Good = observer(() => <div>{store.user.name} {store.user.age}</div>);
// ✅ Тоже хорошо: деструктуризация ВНУТРИ observer
const AlsoGood = observer(() => {
const { name, age } = store.user; // ✅ внутри observer — OK
return <div>{name} {age}</div>;
});

Правило 3: Observer-батчинг — автоматически 🎯

Заголовок раздела «Правило 3: Observer-батчинг — автоматически 🎯»

MobX автоматически батчит обновления. Несколько изменений в одном action приводят к одному рендеру:

// Один action — один рендер (не три!)
store.runInAction(() => {
store.name = 'Яша'; // изменение 1
store.age = 25; // изменение 2
store.city = 'Москва'; // изменение 3
// React перерендерится ОДИН РАЗ после всего этого
});

import { createContext, useContext } from 'react';
import { RootStore } from './stores/RootStore';
const StoreContext = createContext<RootStore | null>(null);
// Провайдер
export function StoreProvider({ children }: { children: React.ReactNode }) {
const [store] = useState(() => new RootStore());
return (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
);
}
// Хук для использования стора
export function useStore() {
const store = useContext(StoreContext);
if (!store) throw new Error('useStore must be used within StoreProvider');
return store;
}
// В компоненте
const CartButton = observer(() => {
const { cartStore } = useStore();
return (
<button onClick={() => cartStore.checkout()}>
🛒 {cartStore.itemCount}
</button>
);
});

Granular updates — чем точнее компонент читает данные, тем реже он рендерится:

// ❌ Весь список рендерится при изменении любого todo
const BadTodoList = observer(() => {
return (
<ul>
{store.todos.map(todo => (
<li key={todo.id}>{todo.text} — {todo.done ? '✓' : '○'}</li>
))}
</ul>
);
});
// ✅ Каждый todo — отдельный observer, рендерится только изменившийся
const GoodTodoList = observer(() => (
<ul>
{store.todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</ul>
));
const TodoItem = observer(({ todo }: { todo: Todo }) => (
<li>{todo.text} — {todo.done ? '✓' : '○'}</li>
// Этот компонент рендерится ТОЛЬКО при изменении THIS todo!
));