Mobx observer не работает

Mobx — неприятные моменты

Я хотел бы поговорить о том, что мне редко попадалось на глаза в статьях о MobX, о тех неприятных моментах, что портят впечатление от использования, а так же о способах решения этих моментов. И не обойдусь без описания плюсов, чтобы оправдать собственный выбор. Начнем.

MobX – менеджер состояния, к этому времени 6 версии, которая работает благодаря Proxy. Далее мнение основано на использовании MobX v6 в связке с библиотекой React при разработке мобильных (React Native) и веб-приложений. Стоит уточнить, что я пользовался в прежних проектах MobX v4, react-easy-state, Redux, Zustand, а также ознакомлен с десятком альтернативных менеджеров состояния на уровне чтения их документации. Так же замечу, что все приведенные далее плюсы и минусы не полны и выведены в сравнении с другими менеджерами состояния.

Плюсы

С приходом 6 версии перестали быть нужны классы и декораторы. И то, и другое я считаю ненужным и даже вредным синтаксическим сахаром, поэтому возможность создавать хранилища посредством объектов в новой версии считаю отличным выбором.
Пример:

Очень естественная работа с хранилищами как с объектами. Отсюда вытекают подсказки типов, обращения к полям, автоимпорт и прочие плюшки – Proxy творят чудеса.
Пример:

Читайте также:  Решил что не буду работать по профессии

Лёгкое изменение состояния. Да, я в целом за имутабельность, но именно здесь я не вижу в ней смысла, так как при необходимости я могу сам создавать полные снимки состояния всех хранилищ, поместив их в один объект и вызывая JSON.stringify или что-то кастомное, если потребуется. В Redux проблема решается подключением immer для глубоко вложенных объектов. И да, все зависит от того, насколько точечным является изменение объекта, и там где можно воспользоваться средствами функционального программирования, ими же и пользуемся.
Пример:

Селекторы. Здесь MobX действительно блистает. Когда необходимы срезы данных лишь на основе хранилищ, используем геттеры, в остальных случаях храним параметризованные селекторы в отдельных файлах с применением computed от MobX computedFn из mobx-utils. При этом все они автоматически мемоизируются MobX, что позволяет надеяться на хорошую производительность приложений. Не то, чтобы в Redux были сложности с reselect, но здесь опять же код и пишется, и читается проще.
Пример:

Минусы

Оборачивание компонентов в observer HOC для добавления им реактивности. Обходится путем написания сниппетов для компонентов и привыканием, как и у любых, основанных не на хуках-селекторах, менеджеров состояния. Плюсом добавляет мемоизацию компонентов, а так как у нас принято мемоизировать любой компонент, то фактически для нас это лишь замена одного вызова на другой.
Пример:

Сбои компонентов при обновлении их кода с хуками на ходу. Конкретнее — добавление или удаление хука с последующим сохранением приводит к сбоям. Дело в плохой работе Fast Refresh с HOC вокруг экспортируемого дефолтного компонента, а не в самом MobX. Чтобы не менять привычную структуру проекта, в которой мы экспортируем компоненты по умолчанию через export default observer(Component), пришлось изучить написание babel-плагинов. Был создан плагин, который переносит вызов observer в объявление функции компонента и убирает вызов из экспорта. Стало хорошо. Конечно, вы скажете, зачем такие заморочки, ведь по докам MobX требуется завернуть компонент именно при объявлении. Отвечу, что при использовании HOC’ов в экспорте код смотрится гораздо красивее и имеет меньшую вложенность. Плюс, смена memo на observer делается проще, там где требуется использовать хранилища MobX.
Пример:

Читайте также:  Как настроить item physic

Необходимость соблюдать осторожность при работе с MobX-объектами внутри компонента. Так как мы работаем с реактивными мутабельными данными, то надеяться на их неизменность при передаче куда-то ещё, в том числе, внутрь других объектов, уже нельзя, в отличие от данных Redux. Например, если мы захотим хранить ту же историю изменений в каком-нибудь редакторе. В таких случаях необходимо помнить о MobX-костыле под названием toJS, который преобразует данные в обычные объекты Javascript.
Пример:

Отсутствие хороших инструментов, аналогичных Redux DevTools. Благо у меня были наработки для react-easy-state, что позволило их дополнить и создать библиотеку для работы MobX с Redux DevTools. Вкратце, в ней я оборачиваю и заменяю все действия MobX на логирующие функции, и создаю снимки состояния хранилищ при из вызове. Мониторить изменения MobX-хранилищ стало легко и приятно.

И конечно не могу не упомянуть, как неудобна отладка Proxy-объектов, ведь именно на них построен MobX 6. Их всегда нужно открывать, чтобы кликнуть по полю target, где и лежит нужный нам объект. Когда выводим логи, то ещё можно обойтись оборачиванием в toJS от MobX, а вот при отладке ещё не придумал решение. Возможно, есть настройка отображения Proxy в браузере и Visual Studio Code, пока что это мне не ведомо.
Пример:

MobX — достойный менеджер состояния, хоть и потребовавший доработки под нужды нашего проекта. Несмотря на небольшие проблемы при отладке Proxy-объектов, простота написания, отличная читаемость кода, а так же хорошая производительность благодаря мемоизации геттеров и computed computedFn от mobx-utils на мой взгляд делают его одним из лучших решений.

Обновление статьи: поправил форматирование кода, исправил грубую ошибку насчет мемоизации computed.

Источник

Применение паттерна observer в Redux и Mobx

Паттерн «observer» известен наверное с момента появления самого ооп. Упрощенно можно представить что есть объект который хранит список слушателей и имеет метод «добавить», «удалить» и «оповестить», а внешний код либо подписывается либо оповещает подписчиков

В redux-е этот паттерн применяется без всяких изменений — пакет «react-redux» предоставляет функцию connect которая оборачивает компонент и при вызове componentDidMount вызовет subscribe() метод у Observable , при вызове componentWillUnmount() вызовет unsubscribе() а dispatch() просто вызовет метод trigger() который в цикле вызовет всех слушателей где каждый в свою очередь вызовет mapStateToProps() и потом в зависимости от того изменилось ли значение — вызовет setState() на самом компоненте. Все очень просто, но платой за такую простоту реализации является необходимость работать с состоянием иммутабельно и нормализировать данные а при изменении отдельного объекта или даже одного свойства оповещать абсолютно всех подписчиков-компонентов даже если они никак не зависят от той измененной части состояния и при этом в компоненте-подписчике необходимо явно указывать от каких частей стора он зависит внутри mapStateToProps()

Mobx очень похож на redux тем что использует этот паттерн observer только развивает его еще дальше — что если мы не будем писать mapStateToProps() а сделаем так чтобы компоненты зависели от данных которые они «рендерят» самостоятельно , по отдельности. Вместо того чтобы собирать подписчиков на одном объекте состояния всего приложения, подписчики будут подписываться на каждое отдельное поле в состоянии. Это как если бы для юзера, у которого есть поля firstName и lastName мы создали бы целый redux-стор отдельно для firstName и отдельно для lastName .

Таким образом, если мы найдем легкий способ создавать такие «сторы» и подписываться на них, то mapStateToProps() будет не нужен, потому что эта зависимость от разных частей состояния уже выражается в существовании разных сторов.

Итак на каждое поле у нас будет по отдельному «мини-стору» — объекту observer где кроме subscribe() , unsubscribe() и trigger() добавится еще поле value а также методы get() и set() и при вызове set() подписчики вызовутся только если само значение изменилось.

Вместе с этим требование иммутабельности стора нужно трактовать немного по-другому — если мы в каждом отдельном сторе будем хранить только примитивные значение, то с точки зрения redux нет ничего зазорного в том чтобы вызвать user.firstName.set(«NewName») — поскольку строка это иммутабельное значение — то здесь происходит просто установка нового иммутабельного значения стора, точно так же как и в redux. В случаях когда нам нужно сохранить в «мини-сторе» объект или сложные структуры то можно просто вынести их в отдельные «мини-сторы». Например вместо этого

лучше написать так чтобы компоненты могли по отдельности зависеть то от «email» то от «address» и чтобы не было лишних «перерендеров»

Второй момент — можно заметить что с таким подходом мы будем вынуждены на каждый доступ к свойству вызывать метод get() , что добавляет неудобств.

Но эта проблема решается через геттеры и сеттеры javascript-а

А если вы не относитесь негативно к декораторам то этот пример можно еще больше упростить

В общем можно пока подвести итоги и сказать что 1) никакой магии в этом моменте нет — декораторы это всего лишь геттеры и сеттеры 2) геттеры и сеттеры всего лишь считывают и устанавливают root-state в «мини-сторе» а-ля redux

Идем дальше — для того чтобы подключить все это к реакту нужно будет в компоненте подписаться на поля которые в нем выводятся и потом отписаться в componentWillUnmount

Да, при росте полей которые выводятся в компоненте, количество болерплейта будет возрастать многократно но одним небольшим движением ручную подписку можно убрать полностью если добавить несколько строчек кода — поскольку в шаблонах так или иначе будет вызываться метод .get() чтобы отрендерить значение то мы можем воспользоваться этим чтобы сделать автоматическую подписку — если перед вызовом метода render() компонента мы запишем в глобальной переменной текущий массив то в методе .get() мы просто добавим this в этот массив и потом в к конце вызова метода render() мы получим массив всех “мини-сторов” на которые подписан текущий компонент. Этот простой механизм решает даже ситуации когда сторы на которые подписан компонент динамически меняются во время рендера — например когда компонент рендерит

Здесь функция connect оборачивает компонент или stateless-component (функцию) реакта и возвращает компонент который благодаря этому механизму автоподписки подписывается на нужные «мини-сторы».

В итоге у нас получился такой вот механизм автоподписок только на нужные данные и оповещений только когда эти данные изменились. Компонент будет обновляться только тогда когда изменились только те «мини-сторы» на которые он подписан. Учитывая, что в реальном приложении, где может быть тысячи этих «мини-сторов», с данным механизмом множественных сторов при изменении одного поля будут обновляться только те компоненты которые находятся в массиве подписчиков на это поле, а вот подходом redux когда мы подписываем все эти тысячи компонентов на один единственный стор, при каждом изменении нужно оповещать в цикле все эти тысячи компонентов (и при этом заставляя программиста вручную описывать от каких частей состояния зависят компоненты внутри mapStateToProps )

Более того этот механизм автоподписок способен улучшить не только redux а и такой паттерн как мемоизацию функций, и заменить библиотеку reselect — вместо того чтобы явно указывать в createSelector() от каких данных зависит наша функция, зависимости будут определяться автоматически точно так же выше сделано с функцией render()

Вывод

Mobx это логичное развитие паттерна observer для решения проблемы «точечных» обновлений компонентов и мемоизации функций. Если немного отрефакторить и вынести код в примере выше из компонента в Observable и вместо вызова .get() и .set() поставить геттеры и сеттеры, то мы почти что получим observable и computed декораторы mobx-а. Почти — потому что у mobx вместо простого вызова в цикле находится более сложный алгоритм вызова подписчиков для того чтобы исключить лишние вызовы computed для ромбовидных зависимостей, но об этом в следующей статье.

Источник

Полная реактивность: подробное объяснение работы MobX

В связи с большим спросом (и для того, чтобы рассказать крутую историю своим внукам) написал статью о внутренней работе MobX. Многих удивляет последовательность и скорость его работы. Но будьте уверены, здесь нет никакой магии!

Сначала давайте определимся с основными терминами:

  1. Наблюдаемое состояние (observable state). Любое значение, которое может быть изменено и может служить источником для вычисленных значений, является состоянием. MobX может сделать большинство типов значений (примитивы, массивы, классы, объекты и т.д.) и даже (потенциально циклические) ссылки наблюдаемыми из коробки.
  2. Вычисленные значения (computed values). Любое значение, которое может быть вычислено с помощью функции, использующей исключительно другие вычисленные значения. Вычисленные значения могут варьироваться от конкатенации нескольких строк до получения сложных объектов и визуализаций. Поскольку вычисленные значения являются наблюдаемыми сами по себе, даже отображение всего пользовательского интерфейса может быть выведено из наблюдаемого состояния. Вычисленные значения могут быть рассчитаны либо лениво, либо как реакция на изменения состояния.
  3. Реакции (reactions). Реакция немного похожа на вычисленное значение, но вместо того, чтобы произвести новое значение, она дает побочный эффект. Реакции соединяют реактивное и императивное программирование для таких вещей, как вывод в консоль, выполнение сетевых запросов, инкрементальное обновление дерева компонентов React для обновления DOM и т.д.
  4. Экшны (actions). Экшны являются основным средством изменения состояния. Экшны не являются реакцией на изменения состояния, а берут источники изменений, такие как пользовательские события или входящие соединения через веб-сокеты, для изменения наблюдаемого состояния.

Вычисленные значения и реакции называются производными в оставшейся части этой статьи. Пока все это может показаться немного академичным, так что давайте добавим конкретики! В электронной таблице все ячейки данных, имеющие значения, будут формировать наблюдаемое состояние. Формулы и графики представляют собой расчетные значения, которые могут быть получены из ячеек данных и других формул. Вывод ячейки данных или формулы на экран является реакцией. Изменение ячейки данных или формулы — это действие.

В любом случае, вот все четыре понятия в маленьком примере, использующем MobX и React:

Мы можем нарисовать дерево зависимостей на основе приведенного выше списка. Интуитивно это будет выглядеть следующим образом:

Состояние этих приложений отображается в наблюдаемых свойствах (синий цвет). Зеленое вычисленное значение fullName может быть выведено из состояния автоматически путем наблюдения за именем и фамилией. Аналогично рендеринг profileView может быть получен из nickName и fullName. profileView будет реагировать на изменения состояния, создавая побочный эффект: он обновляет дерево компонентов React.

При использовании MobX дерево зависимостей имеет минимальный размер. Например, как только человек получает псевдоним, вывод значения fullName, первого имени или фамилии больше не будет влиять на рендеринг (см. рисунок 1). Все отношения наблюдателя между этими значениями могут быть очищены, и MobX автоматически упростит дерево зависимостей соответственно:

Reacting to state changes is always better then acting on state changes.

Любое императивное действие, которое приложение делает в ответ на изменение состояния, обычно создает или обновляет некоторые значения. Другими словами, большинство действий управляют локальным кэшем. Запуск обновления пользовательского интерфейса? Обновление агрегированных значений? Все это можно считать как замаскированная инвалидация кэша. Чтобы обеспечить синхронизацию этого кэша, вам нужно подписаться на будущие изменения состояния, которые позволят вам снова выполнить нужные действия.

Но работа с подписками (или курсорами, линзами, селекторами, коннекторами и т.д.) имеет фундаментальную проблему: по мере развития вашего приложения вы будете совершать ошибки в управлении этими подписками и либо переподписываться (продолжать подписку или сохранять значения, которые больше не используются в компоненте), либо отказываться от подписки (забывая слушать обновления).

Другими словами, при использовании подписок вручную ваше приложение в конечном итоге будет непоследовательным.

Изображение выше является хорошим примером непоследовательности интерфейса Twitter. Как я уже говорил в своем докладе Reactive2015, для этого могут быть только две причины: или отсутствует подписка у твитов, которая обновляет их при изменении данных об авторе. Или данные были нормализованы, и автор твита даже не имеет отношения к профилю текущего вошедшего в систему пользователя, несмотря на то, что обе части данных пытаются описать одни и те же свойства одного и того же человека.

Крупные подписки, в стиле Flux, очень чувствительны к переподписке (oversubscribing). Используя React, вы можете просто определить, переписываются ли ваши компоненты, логируя ненужные рендеринги. MobX уменьшит это число до нуля. Идея проста, но не интуитивна: Увеличение числа подписок приводит к меньшему числу пересчетов. MobX управляет многими тысячами наблюдателей за вас. Вы можете эффективно обменять память на циклы процессора.

Обратите внимание, что переподписка также существует в очень интересных формах. Если вы подписываетесь на данные, которые используются, но не при всех условиях, вы все равно подписываетесь сверх нормы. Например, если компонент profileView подписывается на полное имя человека с nickName, то это переподписка (см. рисунок 1). Так что важным принципом дизайна MobX является принцип:

Минимальный, согласованный набор подписок можно получить только в случае, если подписки определены в рантайме.

Вторая важная идея за MobX заключается в том, что для любого приложения, более сложного, чем TodoMVC, вам часто нужен граф данных, вместо нормализованного дерева, чтобы хранить его мысленно управляемым, но оптимальным образом. Графики обеспечивают согласованность ссылок и исключают дублирование данных, что позволяет гарантировать, что получаемые значения никогда не будут устаревшими.

Как MobX эффективно поддерживает все вычисления в согласованном состоянии

Решение: не кэшировать, а вычислять. Люди спрашивают: “Разве это не слишком дорого?”. Нет, на самом деле это очень эффективно! Причина этого, как объяснялось выше: MobX не выполняет все вычисления, но обеспечивает синхронизацию с наблюдаемым состоянием только вычисленных значений, участвующих в той или иной реакции. Эти производные называются реактивными. Чтобы снова провести параллель с электронными таблицами: при изменении одной из наблюдаемых ячеек данных необходимо пересчитывать только те формулы, которые в данный момент видны или косвенно используются рассматриваемой формулой.

Ленивое вычисление против реактивного вычисления.
Так что насчет вычислений, которые не используются прямо или косвенно реакцией? Вы все еще можете в любое время проверить значение вычисленного значения, например fullName. Решение простое: если вычисленное значение не является реактивным, оно будет вычисляться по требованию (лениво), как и обычная функция-геттер. Ленивые производные (которые никогда ничего не наблюдают) могут быть просто мусором, если они выходят за рамки видимости. Помните, что вычисляемые значения всегда должны быть чистыми функциями наблюдаемого состояния приложения? Вот почему: Для чистых функций не имеет значения, лениво они выполняются или сразу: выполнение чистой функции всегда дает один и тот же результат при одном и том же наблюдаемом состоянии.

Выполнение вычислений
Реакции и вычисленные значения выполняются MobX одним и тем же способом. При повторном запуске вычислений функция добавляется в стек вычислений: стек функций, в котором в данный момент выполняются вычисления. Пока вычисления выполняются, каждый наблюдаемый объект, к которому осуществляется доступ, регистрируется как зависимость от самой верхней функции вычислительного стека. Если требуется значение вычисленной величины, то она может быть последней известной величиной, если вычисленная величина уже находится в реактивном состоянии. Иначе он будет толкать себя на производный стек, переключаться в реактивный режим, а также запускать вычисления.

Распространение изменения состояния

Производные будут автоматически реагировать на изменения состояния. Все реакции происходят синхронно и, что более важно, без сбоев. При изменении наблюдаемого значения выполняется следующий алгоритм:

  1. Наблюдаемый объект посылает всем своим наблюдателям уведомление о том, что он стал устаревшим. Любые затрагиваемые вычисленные значения будут рекурсивно передаваться своим наблюдателям. В результате часть дерева зависимостей будет помечена как устаревшая. В примере дерева зависимостей на рисунке 5 наблюдатели, которые устареют при изменении значения “1”, отмечены оранжевой пунктирной линией. Это все производные, на которые может повлиять изменение значения.
  2. После отправки уведомления об устаревших данных и сохранения нового значения, будет отправлено уведомление о готовности. Это уведомление также указывает на то, действительно ли изменилось значение.
  3. Как только производная получит уведомление о готовности для каждого уведомления об устаревших данных, полученного в шаге 1, она будет знать, что все наблюдаемые значения стабильны и начнет перерасчет. Подсчет количества готовых/устаревших уведомлений гарантирует, что, например, вычисленное значение “4” будет пересматриваться только после того, как вычисленное значение “3” станет стабильным.
  4. Если ни одно из уведомлений о готовности не указывает на то, что значение было изменено, то производная просто скажет собственным наблюдателям, что она снова готова, но без изменения своего значения. В противном случае вычисления будут пересчитаны и отправят собственным наблюдателям уведомление о готовности. В результате получается такой порядок выполнения, как показано на рисунке 5. Обратите внимание, что последняя реакция (помеченная символом “-”) никогда не будет выполнена, если вычисленное значение “4” действительно пересчитано, но не дало нового значения.

В предыдущих двух абзацах кратко описано, как отслеживаются зависимости между наблюдаемыми значениями и производными во время выполнения и как изменения распространяются через производные. В этот момент вы также можете понять, что реакция — это, по сути, вычисленное значение, которое всегда находится в реактивном режиме. Важно понимать, что этот алгоритм может быть реализован очень эффективно без замыканий и только с помощью множества массивов указателей. Кроме того, MobX применяет ряд других оптимизаций, которые выходят за рамки данной заметки в блоге.

Синхронное выполнение

Люди часто удивляются, что MobX работает все синхронно (как RxJ и в отличие от knockout). Это имеет два больших преимущества: Первым делом становится просто невозможным когда-либо наблюдать за устаревшими производными. Таким образом, производная величина может быть использована сразу же после изменения величины, которая влияет на нее. Во-вторых, это упрощает отладку и стэк-трейсинг, так как позволяет избежать бесполезных стэк-трейсов, типичных для библиотек Promise / async.

Однако синхронное исполнение также вводит необходимость в транзакциях. Если сразу несколько мутаций применяются сразу несколько раз подряд, то после применения всех изменений предпочтительнее провести повторное вычисление всех производных. Обертывание действия в блок транзакции позволяет достичь этого. Транзакции просто откладывают все готовые уведомления до тех пор, пока не будет завершен блок транзакций. Обратите внимание, что транзакции все еще выполняются и обновляются синхронно.

Это наиболее важные детали реализации MobX. Мы еще не все рассмотрели, но хорошо знать, например, что вы можете использовать композицию вычисленных значений. Композируя реактивные вычисления, можно даже автоматически преобразовывать один граф данных в другой граф данных и поддерживать его в актуальном состоянии с минимальным количеством изменений. Это упрощает реализацию сложных шаблонов, таких как отображение карты, отслеживание состояния с использованием неизменяемых общих данных или загрузка данных от бэкенда. Но подробнее об этом в следующей статье.

Источник

Оцените статью