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

Шаг 3. Async и RootStore

Финал Части F. Здесь сходятся три темы из React-части: загрузка данных (E3), раздача стора через Context (E5) и персист (E6). Сделаем загрузку из taskApi двумя способамиrunInAction и flow, — соберём RootStore и раздадим его через React Context.

  1. Добавь в TaskStore асинхронную загрузку из taskApi.getTasks() с полями status (loading/success/error) и error.
  2. Реализуй загрузку двумя способами и покажи оба: через async-метод с runInAction и через генератор-flow.
  3. Собери RootStore, содержащий taskStore (и дающий место для роста).
  4. Раздай RootStore через React Context + провайдер StoreProvider + хук useStores (создание стора один раз).
  5. Сохраняй выбранный filter между сессиями через autorun + localStorage.

Тип Task и мок-API taskApi — из Окружения.

  • runInAction(() => {...}) — после await контекст action теряется, и менять наблюдаемые напрямую нельзя (в строгом режиме). runInAction оборачивает изменения состояния после await обратно в action. Простой способ для async.
  • flow (генератор) — альтернатива: пишешь метод-генератор с yield вместо await, и MobX сам оборачивает каждый шаг после yield в action — runInAction не нужен. Объявляется через makeAutoObservable(this, { loadViaFlow: flow }).
  • RootStore — корневой объект, агрегирующий все сторы. Точка роста: позже добавишь authStore, uiStore и т.д., связанные между собой.
  • createContext + useContext + useState(() => new RootStore()) — раздача стора вглубь без prop-drilling; useState с функцией-инициализатором создаёт стор один раз.
  • autorun(() => {...}) — реакция, запускающаяся немедленно и при каждом изменении прочитанных наблюдаемых. Идеальна для сайд-эффекта «сохранить в localStorage».
Показать эталонную реализацию
import { makeAutoObservable, runInAction, flow, autorun, type IReactionDisposer } from 'mobx';
import { createContext, useContext, useState } from 'react';
import type { Task, Filter } from '../types/task';
import { taskApi } from '../api/taskApi';
type Status = 'idle' | 'loading' | 'success' | 'error';
export class TaskStore {
tasks: Task[] = [];
status: Status = 'idle';
error: string | null = null;
filter: Filter = 'all';
constructor() {
// loadViaFlow — генератор, помечаем как flow
makeAutoObservable(this, { loadViaFlow: flow });
}
setFilter(filter: Filter) {
this.filter = filter;
}
// === Способ 1: async + runInAction ===
async loadTasks() {
this.status = 'loading';
this.error = null;
try {
const tasks = await taskApi.getTasks();
// после await — изменения только через runInAction
runInAction(() => {
this.tasks = tasks;
this.status = 'success';
});
} catch (err) {
runInAction(() => {
this.error = err instanceof Error ? err.message : 'Ошибка';
this.status = 'error';
});
}
}
// === Способ 2: flow (генератор) — runInAction не нужен ===
*loadViaFlow() {
this.status = 'loading';
this.error = null;
try {
// yield вместо await; присваивание после yield — уже в action
const tasks: Task[] = yield taskApi.getTasks();
this.tasks = tasks;
this.status = 'success';
} catch (err) {
this.error = err instanceof Error ? err.message : 'Ошибка';
this.status = 'error';
}
}
addTask(title: string) {
const trimmed = title.trim();
if (!trimmed) return;
this.tasks.push({ id: crypto.randomUUID(), title: trimmed, done: false, createdAt: Date.now() });
}
toggleTask(id: string) {
const task = this.tasks.find((t) => t.id === id);
if (task) task.done = !task.done;
}
removeTask(id: string) {
this.tasks = this.tasks.filter((t) => t.id !== id);
}
}
// === RootStore — агрегатор всех сторов ===
export class RootStore {
taskStore: TaskStore;
constructor() {
this.taskStore = new TaskStore();
this.restoreFilter();
this.setupPersistence();
}
// Восстановление сохранённого фильтра при старте
private restoreFilter() {
try {
const saved = localStorage.getItem('task-filter');
if (saved) this.taskStore.setFilter(JSON.parse(saved) as Filter);
} catch {
/* ignore */
}
}
// Персист фильтра через autorun: запускается сразу и при каждом изменении filter
private setupPersistence(): IReactionDisposer {
return autorun(() => {
// читаем this.taskStore.filter → autorun подпишется именно на него
localStorage.setItem('task-filter', JSON.stringify(this.taskStore.filter));
});
}
}
// === Раздача через Context ===
const StoreContext = createContext<RootStore | null>(null);
export function StoreProvider({ children }: { children: React.ReactNode }) {
// useState с инициализатором — RootStore создаётся один раз
const [store] = useState(() => new RootStore());
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
}
export function useStores(): RootStore {
const store = useContext(StoreContext);
if (!store) throw new Error('useStores must be used within StoreProvider');
return store;
}

Использование в компоненте (observer + хук useStores):

import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import { useStores } from '../stores/RootStore';
export const TaskApp = observer(function TaskApp() {
const { taskStore } = useStores();
useEffect(() => {
taskStore.loadTasks(); // или taskStore.loadViaFlow()
}, [taskStore]);
if (taskStore.status === 'loading') return <p>Загрузка...</p>;
if (taskStore.status === 'error') return <p>Ошибка: {taskStore.error}</p>;
return (
<ul>
{taskStore.tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
});

Ты переписал Task Tracker на MobX: makeAutoObservable-стор, computed-производные, observer-компоненты, async через runInAction и flow, RootStore с Context и персист через autorun.

Теперь у тебя есть один и тот же проект в двух парадигмах. Сравни код шага E6 и этой страницы — и ты почувствуешь, где MobX экономит ручную работу, а где хуки дают больше контроля.