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

от автора

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

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

Плюсы

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

import { makeAutoObservable } from 'mobx'; export const chatStore = makeAutoObservable({   chats: {},   messages: {},   createChat: () => {},   createMessage: () => {}, });

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

import { chatStore } from 'stores'; const Chat = () => {   const messages = chatStore.messages;   const onPress = chatStore.createMessage;   return null; };

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

import { makeAutoObservable } from 'mobx'; export const chatStore = makeAutoObservable({     messages: {},   createMessage: (message) => {     chatStore.messages[message.id] = message;   }, });

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

import { makeAutoObservable } from 'mobx'; import { profileStore } from 'stores/profileStore'; export const chatStore = makeAutoObservable({   messages: {},   get myMessages() {     return Object.values(messages).filter((message) => message.userId === profileStore.userId);     } });

Минусы

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

import { observer } from 'mobx-react-lite'; const Chat = () => null; export default observer(Chat);

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

import { observer } from 'mobx-react-lite'; const Chat = () => {   // удаление или добавление хука приведет к сбою fast refresh   const [visible, setVisible] = useState(false);   return null; }; export default observer(Chat);

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

import { chatStore } from 'stores'; import { useRef } from 'react'; import { toJS} from 'mobx'; const Chat = () => {   const messages = chatStore.messages;   const prevMessages = useRef();   const onPress = () => {     const messagesPurified = toJS(messages);     prevMessages.current = messagesPurified;     // не вызвать перед этим toJS = выстрелить себе в ногу   };   return null; };

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

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

p = new Proxy({}, {}) p >  Proxy {}       [[Handler]]: Object       [[Target]]: Object       [[IsRevoked]]: false

Итог

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/570236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *