Pull to refresh

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

Reading time5 min
Views22K

Я хотел бы поговорить о том, что мне редко попадалось на глаза в статьях о 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.

Tags:
Hubs:
Total votes 12: ↑9 and ↓3+6
Comments24

Articles