62. MobX + React
MobX + React: Observer и реактивные компоненты 🔗
Заголовок раздела «MobX + React: Observer и реактивные компоненты 🔗»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 — не будет реагировать на изменения MobXfunction BadCounter() { return <div>{counterStore.count}</div>; // ❌ не обновится!}
// С observer — автоматически перерендерится при изменении countconst GoodCounter = observer(() => { return <div>{counterStore.count}</div>; // ✅ работает!});
// Или для именованного компонентаconst Counter = observer(function Counter() { return ( <button onClick={() => counterStore.increment()}> Клики: {counterStore.count} </button> );});2. Как работает observer() под капотом 🔧
Заголовок раздела «2. Как работает observer() под капотом 🔧»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: как избежать проблем ⚠️»Правило 1: Каждый компонент — observer() 🔍
Заголовок раздела «Правило 1: Каждый компонент — observer() 🔍»// ❌ Плохо: только родитель — 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! 📦»// ❌ Плохо: деструктуризация разрывает связь с observableconst { name, age } = store.user; // MobX теряет трекинг!const Broken = () => <div>{name} {age}</div>; // не обновится!
// ✅ Хорошо: читаем свойства прямо в renderconst Good = observer(() => <div>{store.user.name} {store.user.age}</div>);
// ✅ Тоже хорошо: деструктуризация ВНУТРИ observerconst 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 перерендерится ОДИН РАЗ после всего этого});5. Context + MobX — распределяем сторы 🌐
Заголовок раздела «5. Context + MobX — распределяем сторы 🌐»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> );});6. Оптимизация рендеров 🚀
Заголовок раздела «6. Оптимизация рендеров 🚀»Granular updates — чем точнее компонент читает данные, тем реже он рендерится:
// ❌ Весь список рендерится при изменении любого todoconst 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!));