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

MobX + React интеграция

Окно терминала
npm install mobx mobx-react-lite

mobx-react-lite — лёгкая версия только для функциональных компонентов (рекомендуется).

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();
// ❌ Деструктурированные значения — это обычные переменные, не reactive
const { users, isLoading } = store; // isLoading больше не reactive!
observer(() => <div>{isLoading ? 'Загрузка' : users.length}</div>);
// ✅ Читай через точку
observer(() => <div>{store.isLoading ? 'Загрузка' : store.users.length}</div>);
// Правило: 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 через store
function 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 не нужен

Создаёт локальное 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>
);
});
// useState: хорошо для простого состояния
function SimpleForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return /* ... */;
}
// useLocalObservable: лучше для связанного состояния с computed и actions
const 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>
);
});
// Для изолированных виджетов — передача store как prop
interface 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 observable
const 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 компонент перерисовывается целиком
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>
);
});
// Только этот компонент перерисовывается при изменении item
const SmartItem = observer(function SmartItem({
item,
store
}: {
item: Item;
store: Store;
}) {
return (
<div onClick={() => store.select(item.id)}>
{item.text}
</div>
);
});