Шаг 2. computed + observer
Эквивалент шага E2 (useMemo), но без массивов зависимостей. Добавим фильтр, поиск и счётчики как computed, а компоненты сделаем реактивными через observer.
Постановка задачи
Заголовок раздела «Постановка задачи»- Добавь в
TaskStoreнаблюдаемые поляfilter: Filterиsearch: string+ actionssetFilter,setSearch; - добавь
computed-геттеры:filteredTasks(фильтр all/active/done + поиск) и счётчикиtotal,activeCount,doneCount; - оберни компоненты, читающие стор, в
observer— они должны перерисовываться автоматически при изменении наблюдаемых; - соблюдай правило «читай через точку»: обращайся к
store.filteredTasks, не деструктурируй наблюдаемые.
Тип Filter — из src/types/task.ts.
Какие API и зачем
Заголовок раздела «Какие API и зачем»computed(черезget-геттер) — производное состояние.makeAutoObservableделает геттеры вычисляемыми: они кэшируются и пересчитываются только при изменении наблюдаемых, которые читают. Прямой аналогuseMemo, но зависимости отслеживаются сами.observer(Component)изmobx-react-lite— подписывает компонент на наблюдаемые, которые он читает во время рендера, и перерисовывает при их изменении. Нужен на каждом компоненте, который читает стор.- Чтение «через точку» —
observerотслеживает чтения наблюдаемых. Если деструктурировать (const { filteredTasks } = store), значение «отвязывается» от наблюдаемого, и реактивность теряется. Читайstore.filteredTasksпрямо в JSX.
Показать эталонную реализацию
import { makeAutoObservable } from 'mobx';import { observer } from 'mobx-react-lite';import type { Task, Filter } from '../types/task';
export class TaskStore { tasks: Task[] = []; filter: Filter = 'all'; search = '';
constructor() { makeAutoObservable(this); }
// actions 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); } setFilter(filter: Filter) { this.filter = filter; } setSearch(search: string) { this.search = search; }
// computed — производное состояние, кэшируется, deps отслеживаются сами get filteredTasks(): Task[] { const byFilter = this.tasks.filter((t) => { if (this.filter === 'active') return !t.done; if (this.filter === 'done') return t.done; return true; }); const query = this.search.trim().toLowerCase(); if (!query) return byFilter; return byFilter.filter((t) => t.title.toLowerCase().includes(query)); }
get total() { return this.tasks.length; } get activeCount() { return this.tasks.filter((t) => !t.done).length; } get doneCount() { return this.total - this.activeCount; }}
export const taskStore = new TaskStore();
// observer: компонент реактивен. Читаем строго через точку (store.xxx)export const TaskBoard = observer(function TaskBoard() { const store = taskStore; const filters: Filter[] = ['all', 'active', 'done'];
return ( <div> <input value={store.search} onChange={(e) => store.setSearch(e.target.value)} placeholder="Поиск..." />
<div> {filters.map((f) => ( <button key={f} disabled={store.filter === f} onClick={() => store.setFilter(f)} > {f} ({f === 'all' ? store.total : f === 'active' ? store.activeCount : store.doneCount}) </button> ))} </div>
<ul> {store.filteredTasks.map((task) => ( <li key={task.id}> <input type="checkbox" checked={task.done} onChange={() => store.toggleTask(task.id)} /> <span style={{ textDecoration: task.done ? 'line-through' : 'none' }}> {task.title} </span> <button onClick={() => store.removeTask(task.id)}>×</button> </li> ))} </ul> </div> );});Ловушки
Заголовок раздела «Ловушки»Дальше — Шаг 3: async и RootStore: загрузка из taskApi, RootStore + Context, персист.