State Management

MobX: observable, computed, action

В корзине интернет-магазина есть товары, у каждого цена и количество. Нужны итоговая сумма, число позиций, флаг пустоты. В ручной модели после каждого изменения товара приходится не забыть пересчитать сумму, обновить счётчик, дёрнуть перерисовку. MobX задаёт вопрос: а что, если объявить items наблюдаемым, total пометить как computed, а дальше просто менять items, и сумма с перерисовкой случатся сами, потому что MobX знает, что total читает items.

  • Корзины и каталоги, где итоговые суммы и счётчики выводятся из списка позиций
  • Формы с валидацией, где признаки isValid и список ошибок зависят от полей
  • Дашборды и таблицы с фильтрами, сортировкой и производными агрегатами
  • Доменные модели в OOP-стиле: классы заказа, пользователя, проекта с методами
  • Приложения, мигрировавшие с ручного дёрганья ре-рендеров на автоматическое отслеживание

Предварительные знания

  • Нативный Proxy и перехват чтения и записи полей
  • Идея производного значения, которое вычисляется из других данных
  • Базовое знакомство с классами и методами в TypeScript
  • Observable через Proxy

Откуда взялось прозрачное отслеживание

MobX создал Михель Вестстрате в 2015 году, сначала под именем Mobservable. Идея пришла из реактивных таблиц вроде Excel: ячейка-формула сама пересчитывается, когда меняются ячейки, на которые она ссылается, и никто вручную не связывает их. Вестстрате перенёс это в JavaScript: пометить данные как observable, производные как computed, и пусть библиотека сама строит граф зависимостей, наблюдая, что было прочитано при вычислении. К версии MobX 6 базовый способ объявления стал лаконичным: makeAutoObservable в конструкторе класса размечает все поля и методы автоматически.

observable и makeAutoObservable

Состояние в MobX живёт в классе-сторе. В конструкторе вызывается makeAutoObservable(this), и MobX размечает члены класса автоматически: обычные поля становятся observable, геттеры становятся computed, методы становятся action. Дальше с полями работают как с обычными свойствами объекта, а реактивность держит библиотека.

Под капотом MobX оборачивает наблюдаемые поля так, что чтение поля внутри реактивного контекста регистрирует зависимость, а запись уведомляет всех, кто это поле читал. Когда count меняется в increment, MobX знает, какие computed и какие компоненты читали count, и пересчитывает или перерисовывает только их.

makeAutoObservable удобен, но размечает всё подряд. Когда нужен точный контроль, есть makeObservable, где для каждого члена явно указывают observable, computed или action. В большинстве store-классов хватает автоматического варианта, а ручной берут для тонких случаев.

makeAutoObservable не умеет работать с наследованием: для класса с предком нужен makeObservable с явной разметкой. Также автоматический вариант не размечает поля, добавленные вне конструктора. Поэтому всё наблюдаемое состояние объявляют как поля класса, а makeAutoObservable вызывают в конце конструктора.

Что делает makeAutoObservable(this) в конструкторе стор-класса?

computed и автоматическое отслеживание

Производные значения объявляются как геттеры и автоматически становятся computed. Computed вычисляет значение из других observable и computed, кэширует результат и пересчитывает его только тогда, когда меняется что-то из его источников. Между изменениями повторное чтение computed возвращает закэшированное значение без повторного вычисления.

Главное здесь это автоматическое отслеживание зависимостей. Геттер total читает this.items, поэтому MobX запоминает, что total зависит от items. После addItem массив items меняется, MobX помечает total устаревшим, и при следующем чтении total пересчитается. Никто вручную не указывал эту связь: она выведена из того, что было прочитано при вычислении.

  • Ручной пересчёт — После каждой мутации items нужно не забыть обновить total и count. Пропущенный пересчёт даёт рассинхрон данных
  • computed в MobX — total и count объявлены геттерами. MobX сам знает, что они читают items, и пересчитывает их при изменении

Computed кэшируется, поэтому тяжёлые вычисления вроде фильтрации и агрегации списка дёшево читать многократно: пересчёт случится только при изменении источников. Если же computed нигде не наблюдается, MobX не держит кэш и вычисляет по требованию, не тратя память зря.

Почему геттер total в стор-классе пересчитывается после добавления товара, хотя связь с items нигде не прописана вручную?

action и OOP-дружественная модель

Методы, меняющие состояние, становятся action. Action работает как транзакция. Все изменения observable внутри одного action группируются, и наблюдатели уведомляются один раз после завершения метода, а не после каждого присваивания. Если метод меняет три поля, ре-рендер случится один, а не три.

Метод fill меняет три поля, но это один action, поэтому fullName пересчитается один раз, и наблюдающий компонент перерисуется один раз. Без группировки три присваивания дали бы три уведомления. Транзакционность action это и про корректность, и про производительность: промежуточные несогласованные состояния наружу не видны.

Сама форма записи раскрывает OOP-дружественность MobX. Состояние и поведение живут вместе в классе: данные это поля, производные это геттеры, изменения это методы. Не нужно разносить логику по отдельным action-creator, reducer и селекторам, как во Flux-подходе. Доменная модель заказа или пользователя описывается как обычный класс, а реактивность добавляется одним вызовом в конструкторе.

Когда мутация происходит в асинхронном коде после await, MobX в строгом режиме требует обернуть её в action или runInAction, потому что продолжение после await это уже новый контекст. Это поддерживает правило: всё, что меняет observable, проходит через action.

Зачем метод, меняющий несколько observable-полей, оформляют как action?

Связь с другими темами

Этот урок открывает группу MobX. Дальше идут реакции и архитектура:

  • Observable через Proxy — MobX делает поля наблюдаемыми через тот же Proxy, что разбирается в уроке про реактивность
  • MobX: reactions — autorun, reaction и when запускают побочные эффекты при изменении observable
  • MobX: архитектура — Доменные store-классы строятся на observable, computed и action

Итог

  • makeAutoObservable в конструкторе размечает поля как observable, геттеры как computed, методы как action
  • observable это наблюдаемое состояние, изменение которого автоматически уведомляет зависимых
  • computed это производное значение, оно кэшируется и пересчитывается только при изменении своих источников
  • action группирует мутации в одну транзакцию, чтобы наблюдатели обновились один раз после всех изменений
  • MobX сам строит граф зависимостей, наблюдая что было прочитано при вычислении computed или рендере
  • Модель дружественна OOP: состояние и поведение живут в классе, а не в отдельных action и reducer

Связанные уроки

  • sm-08-observable-proxy — MobX делает поля наблюдаемыми через Proxy, поэтому механика перехвата нужна заранее
  • sm-23-mobx-reactions — Поняв observable и computed, дальше разбираем реакции на их изменение
  • sm-24-mobx-architecture — Доменные store-классы строятся именно на observable, computed и action
MobX: observable, computed, action

0

1

Войти