MobX + React интеграция
Установка
Заголовок раздела «Установка»npm install mobx mobx-react-litemobx-react-lite — лёгкая версия только для функциональных компонентов (рекомендуется).
observer
Заголовок раздела «observer»observer — HOC который делает компонент реактивным: автоматически перерисовывается при изменении используемых observable.
import { observer } from 'mobx-react-lite';import { useUserStore } from '../stores';
// Способ 1: обёртка функцииconst UserList = observer(function UserList() { const store = useUserStore();
return ( <ul> {store.users.map(user => ( <li key={user.id} onClick={() => store.select(user.id)}> {user.name} {store.selectedId === user.id && ' ✓'} </li> ))} </ul> );});
// Способ 2: стрелочная функция (теряется displayName!)const UserCount = observer(() => { const store = useUserStore(); return <span>{store.users.length} пользователей</span>;});// Рекомендуется задать displayName для дебага:UserCount.displayName = 'UserCount';const store = useUserStore();
// ❌ Деструктурированные значения — это обычные переменные, не reactiveconst { users, isLoading } = store; // isLoading больше не reactive!observer(() => <div>{isLoading ? 'Загрузка' : users.length}</div>);
// ✅ Читай через точкуobserver(() => <div>{store.isLoading ? 'Загрузка' : store.users.length}</div>);Где нужен observer?
Заголовок раздела «Где нужен observer?»// Правило: observer нужен там где ЧИТАЮТСЯ observable
// ❌ Компонент не использует observable → observer бесполезенconst PureTitle = observer(({ title }: { title: string }) => ( <h1>{title}</h1>));// Лучше: const PureTitle = ({ title }: { title: string }) => <h1>{title}</h1>;
// ✅ Компонент читает observable → observer нуженconst UserName = observer(function UserName() { const { currentUser } = useAuthStore(); return <span>{currentUser?.name ?? 'Гость'}</span>;});
// ⚠️ Если родитель observer, а дочерний нет — дочерний не перерисуется при изменении observable// передаваемых как props через storefunction Parent() { const store = useUserStore(); // store.users читается здесь — Parent должен быть observer return <Child count={store.users.length} />; // передаём примитив — Child не нужен observer}
const Parent2 = observer(Parent);const Child = ({ count }: { count: number }) => <span>{count}</span>; // observer не нуженuseLocalObservable
Заголовок раздела «useLocalObservable»Создаёт локальное observable состояние внутри компонента:
import { useLocalObservable, observer } from 'mobx-react-lite';
const Counter = observer(function Counter() { const state = useLocalObservable(() => ({ count: 0, step: 1,
get doubled() { return this.count * 2; },
increment() { this.count += this.step; },
decrement() { this.count -= this.step; },
setStep(step: number) { this.step = step; }, }));
return ( <div> <button onClick={state.decrement}>-</button> <span>{state.count} (×2={state.doubled})</span> <button onClick={state.increment}>+</button> <input type="number" value={state.step} onChange={e => state.setStep(Number(e.target.value))} /> </div> );});useLocalObservable vs useState
Заголовок раздела «useLocalObservable vs useState»// useState: хорошо для простого состоянияfunction SimpleForm() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); return /* ... */;}
// useLocalObservable: лучше для связанного состояния с computed и actionsconst ComplexForm = observer(function ComplexForm() { const form = useLocalObservable(() => ({ name: '', email: '', get isValid() { return this.name.length > 0 && this.email.includes('@'); }, setField<K extends 'name' | 'email'>(field: K, value: string) { this[field] = value; }, reset() { this.name = ''; this.email = ''; }, }));
return ( <form> <input value={form.name} onChange={e => form.setField('name', e.target.value)} /> <input value={form.email} onChange={e => form.setField('email', e.target.value)} /> <button disabled={!form.isValid}>Отправить</button> </form> );});Паттерн: observer + Store через props
Заголовок раздела «Паттерн: observer + Store через props»// Для изолированных виджетов — передача store как propinterface TaskListProps { store: TaskStore;}
const TaskList = observer(function TaskList({ store }: TaskListProps) { return ( <div> {store.isLoading && <Spinner />} {store.tasks.map(task => ( <TaskItem key={task.id} task={task} onToggle={() => store.toggleTask(task.id)} onDelete={() => store.deleteTask(task.id)} /> ))} </div> );});
// TaskItem тоже должен быть observer если task — MobX observableconst TaskItem = observer(function TaskItem({ task, onToggle, onDelete}: { task: Task; onToggle: () => void; onDelete: () => void;}) { return ( <div style={{ textDecoration: task.completed ? 'line-through' : 'none' }}> <input type="checkbox" checked={task.completed} onChange={onToggle} /> {task.text} <button onClick={onDelete}>×</button> </div> );});Оптимизация: Observer Children паттерн
Заголовок раздела «Оптимизация: Observer Children паттерн»// Проблема: большой observer компонент перерисовывается целикомconst BigList = observer(function BigList() { const store = useStore(); return ( <div> <h1>Список</h1> {/* При изменении любого элемента — весь список */} {store.items.map(item => <div key={item.id}>{item.text}</div>)} </div> );});
// Решение: вынести реактивные части в отдельные мелкие observer компонентыconst SmartList = observer(function SmartList() { const store = useStore(); return ( <div> <h1>Список</h1> {store.items.map(item => ( <SmartItem key={item.id} item={item} store={store} /> ))} </div> );});
// Только этот компонент перерисовывается при изменении itemconst SmartItem = observer(function SmartItem({ item, store}: { item: Item; store: Store;}) { return ( <div onClick={() => store.select(item.id)}> {item.text} </div> );});