Шаг 3. Async и RootStore
Финал Части F. Здесь сходятся три темы из React-части: загрузка данных (E3), раздача стора через Context (E5) и персист (E6). Сделаем загрузку из taskApi двумя способами — runInAction и flow, — соберём RootStore и раздадим его через React Context.
Постановка задачи
Заголовок раздела «Постановка задачи»- Добавь в
TaskStoreасинхронную загрузку изtaskApi.getTasks()с полямиstatus(loading/success/error) иerror. - Реализуй загрузку двумя способами и покажи оба: через
async-метод сrunInActionи через генератор-flow. - Собери
RootStore, содержащийtaskStore(и дающий место для роста). - Раздай
RootStoreчерез React Context + провайдерStoreProvider+ хукuseStores(создание стора один раз). - Сохраняй выбранный
filterмежду сессиями черезautorun+localStorage.
Тип Task и мок-API taskApi — из Окружения.
Какие API и зачем
Заголовок раздела «Какие API и зачем»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> );});Ловушки
Заголовок раздела «Ловушки»Часть F завершена
Заголовок раздела «Часть F завершена»Ты переписал Task Tracker на MobX: makeAutoObservable-стор, computed-производные, observer-компоненты, async через runInAction и flow, RootStore с Context и персист через autorun.
Теперь у тебя есть один и тот же проект в двух парадигмах. Сравни код шага E6 и этой страницы — и ты почувствуешь, где MobX экономит ручную работу, а где хуки дают больше контроля.