react mobx что это
Mobx — управление состоянием вашего приложения
MobX это простое, опробованное в бою решение для управления состоянием вашего приложения. Этот туториал научит вас основным концептам MobX. MobX это автономная библиотека, но большинство используют ее в связке с React и этот туториал будет сфокусирован на этой комбинации.
Основная идея
Состояние (state ориг.) это сердце каждого приложения и нет более быстрого способа создания забагованого, неуправляемого приложения, как отсутствие консистентности состояния. Или состояние, которое несогласованно с локальными переменными вокруг. Поэтому множество решений по управлению состоянием пытаются ограничить способы, которыми можно его изменять, например сделать состояние неизменяемым. Но это порождает новые проблемы, данные нуждаются в нормализации, нет гарантии ссылочной целостности и становится почти невозможно использовать такие мощные концепты как прототипы(prototypes ориг.).
MobX позволяет сделать управление состоянием вновь простым, вернувшись к корню проблемы: он делает невозможным инконсистентность состояния. Стратегия достижения этого довольно проста: убедится что, все что может быть вынуто из состояния, будет вынуто. Автоматически.
Концептуально MobX обрабатывает ваше приложение как электронная таблица (отсылка к офисной программе для работы с таблицами прим. пер.).
Во-первых, есть состояние State приложения. Графы объектов, массивов, примитивов, ссылок которые формируют модель вашего приложения.
Во-вторых есть производные Derivations. Обычно, это любое значение, которое может быть вычислено автоматически из данных состояния вашего приложения.
Реакции Reactions очень похожи на производные Derivations. Основное отличие: они не возвращают значение, но запускаются автоматически, чтобы выполнить какую то работу. Обычно это связано с I/O. Они проверяют, что DOM обновился или сетевые запросы выполнились вовремя
Наконец, есть действия Actions. Действия это все те штуки которые меняют состояние. MobX проследит, чтобы все изменения в состоянии приложения, вызванные действиями, автоматически обработались всеми производными и реакциями. Синхронно и без помех.
Простой todo store.
Довольно теории, рассмотрим его в действии, будет намного понятнее, чем внимательно читать написанное выше. Ради оригинальности давайте начнем с очень простого Todo хранилища. Ниже приведен очень простой TodoStore, который управляет коллекцией todo. MobX пока не участвует.
Мы только что создали todoStore инстанс с коллекцией todos. Теперь надо заполнить todoStore какими-нибудь объектами. Чтобы убедиться, что от наших изменений есть эффект, мы вызываем todoStore.report после каждого изменения:
Становимся реактивными
До сих пор в нашем коде не было ничего необычного. Но что если мы не хотим вызывать report явно, но объявим что нужно вызывать этот метод на каждое изменение состояния? Это освободит нас от обязанности вызывать этот метод в нашем коде. Мы должны быть уверены в том, что последний результат вызова report будет выведен на экран. Но мы не хотим беспокоится, каким образом это будет сделано.
К счастью, именно MobX может сделать это за нас. Автоматически вызывать код, который зависит от состояния. Так что наша функция report будет вызываться автоматически. Чтобы этого достичь TodoStore нужно стать отслеживаемым (observable ориг.), чтобы MobX смог следить за всеми изменениями. Давайте немного изменим наш класс.
Вот и все! Мы пометили некоторые свойства как @observable чтобы MobX знал что они могут изменяться со временем. Расчеты помечены @computed декораторами, чтобы знать что они могут быть вычислены на основе состояния.
Свойство pendingRequests и assignee еще не используются, но мы увидим их в действии чуть ниже. Для краткости, все примеры используют ES6, JSX и декораторы. Но не беспокойтесь, все декораторы в MobX имеют ES5 аналоги.
Делаем React реактивным
Следующий листинг показывает, что мы просто должны изменить наши данные. MobX автоматически вычислит и обновит соответствующие части вашего пользовательского интерфейса из состояния в вашем хранилище.
Работа со ссылками
Теперь у нас есть два независимых хранилища. Одно с людьми, другое с задачами. Чтобы назначить свойству assignee персону из хранилища с персонами, нам нужно просто присвоить значение через ссылку. Эти значения подхватятся TodoView автоматически. С MobX нет нужды в нормализации данных и написании селекторов, чтобы наши компоненты обновлялись. На самом деле, не имеет значения где хранятся данные. Пока объекты «наблюдаемы», MobX будет отслеживать их. Настоящие JavaScript ссылки тоже работают. MobX отслеживает их автоматически если они релевантны для производных значений.
Асинхронные действия
Так как все в нашем маленьком todo приложении является производным от состояния, то не имеет значения где это состояние будет изменено. Это позволяет достаточно просто создавать асинхронные действия.
DevTools
Пакет mobx-react-devtools предоставляет инструментарий разработчика, который может быть использован в любом MobX + React приложении.
Вывод
На этом все! Никакого бойлерплейта. Простые и декларативные компоненты которые формируют UI легко и просто. Полностью обновляются из состояния. Теперь вы готовы начать использовать пакеты mobx и mobx-react в вашем приложении.
Краткое резюме вещей которые вы сегодня узнали:
MobX не контейнер состояния
Люди часто используют MobX как альтернативу Redux. Но пожалуйста, обратите внимание, что это просто библиотека, для решения определенной проблемы а не архитектура или контейнер состояния. В этом смысле приведенные выше примеры являются надуманными и рекомендуется использовать правильные архитектурные решения, как инкапсуляция логики в методах, их организация в хранилищах или контроллерах и т.д. Или как кто то написал на Hacker News:
«Использовать MobX означает использование контроллеров, диспетчеров, действий, супервизоров или любой другой формы управления потоком данных, это ведет нас к тому, что архитектурную потребность вашего приложения проектируете вы сами, а не используя то что используют по умолчанию для чего то большего чем Todo приложение»
Заинтригованы? Вот некоторые полезные ссылки (на английском прим. пер.):
Почему мы выбрали MobX, а не Redux, и как его использовать эффективнее
Меня зовут Назим Гафаров, я разработчик интерфейсов в Mail.ru Cloud Solutions. На дворе 2020 год, а мы продолжаем обсуждать «нововведения» ES6-синтаксиса и преимущества MobX над Redux. Существует много причин использовать Redux в своем проекте, но так как я не знаю ни одной, расскажу о том, почему мы выбрали MobX.
Как мы пришли к использованию MobX
Mail.ru Cloud Solutions — это платформа облачных сервисов. С точки зрения разработчика, у нас типичная React-админка, которая позволяет полностью управлять облачной средой: создавать виртуальные машины, базы данных и кластеры Kubernetes. Можно скачивать отчеты по балансу, смотреть графики по нагрузке и тому подобное.
Текущий стек — TypeScript, React, Redux, Formik — нас полностью устраивал, за исключением Redux. В какой-то момент к нам пришли с задачей разработать новый проект — админку для платформы интернета вещей. Так как это был новый проект — на отдельном домене и со своим дизайном, мы решили посмотреть в сторону MobX.
Почему не Redux
Многословность
Наверное, всем надоели завывания про многословность Redux, но это реальная проблема. Посмотрите код обычного счетчика. Итак, чтобы изменить состояние нам нужны экшены:
Программисты, которым этого мало, создают actionTypes:
Дальше пишем редьюсеры:
Мапим State и Dispatch — непонятно зачем, но почему бы и нет:
Дальше нам осталось всего лишь законнектить компонент:
А, нет, еще не все. Еще нужно проинициализировать Store:
И прокинуть наш Store дальше в приложение через Provider:
Теперь тот же самый пример, но на MobX. Верстка:
Я могу понять многословность в обмен на какую-то пользу. Например, статические гарантии в обмен на многословность типизации. Но многословность ради многословности. Зачем? Для чего?
Хотя проблема частично решается хуками, никому не хочется писать лишний бесполезный код, и поэтому появилось 100500 библиотек для борьбы с этим бойлерплейтом. Например:
У нас тоже была подобная обертка, она называется Redux-helper. Я думаю, что каждый уважающий себя фронтендер должен написать обертку над Redux для борьбы с бойлерплейтом. Проблема в том, что если вы пытаетесь так улучшить Redux, то у вас случайно получается MobX.
Проблема выбора
Пример со счетчиком был полностью синхронным. Давайте напишем асинхронный код на MobX:
Воспроизводить тот же код на Redux я не буду пытаться даже за 300 баксов, так как это займет слишком много времени.
Отсутствие асинхронности в Redux — на самом деле странное решение. В моем детстве тебя могли избить и за меньшую оплошность.
Для решения внезапно возникшей проблемы асинхронности (никто не ожидал ее в вебе), сообщество создало redux-thunk, redux-saga, redux-observable и redux-loop.
Таким образом, любую проблему Redux можно решить с помощью другой проблемы:
Нужна асинхронность — добавь redux-thunk.
Хочешь меньше бойлерплейта — возьми redux-zero.
Reselect для мемоизации селекторов.
Normalizr для хранения данных в нормализованном виде.
Еще нужен Immutable.js, чтобы постоянно не писать спреды.
Ну и хотелось бы писать в мутабельном стиле, поэтому добавим immer (от автора MobX!)
Вам нужно всё это изучить и выбрать какую-то одну комбинацию. В React-экосистеме и так приходится выбирать библиотеку для форм, библиотеку для запросов в сеть и кучу всего еще. Redux это только усугубляет, потому что сам по себе он неработоспособен, к нему нужно подключать еще что-то.
В итоге каждый проект на Redux — множество чужих спорных решений с модными на тот момент библиотеками. Получается «Франкенштейн», поддерживать который приходится вам, а не тому, кто его создал.
Скорость разработки
Азат Разетдинов в своем докладе рассказывал, как увеличилась производительность его команды после внедрения MobX. А вот еще один отзыв, его автор утверждает, что скорость разработки увеличилась в три раза.
Это не единичные мнения. Тысячи других разработчиков на Stateofjs.com пишут, что раздутость (Bloated) и корявость стиля (Clumsy) — одни из самых нелюбимых аспектов Redux. Раздутый код дольше писать и сложнее поддерживать.
Раздутость кода — это одна из причин, почему инопланетяне еще не вышли с нами на связь.
Производительность
Скорость работы MobX не зависит от количества компонентов, потому что мы заранее знаем список компонентов, которые надо обновить, — вместо O(n) сложности Redux.
Тормознутость Redux вшита в его парадигму. В JavaScript очень дорогая иммутабельность, и вы создаете огромную нагрузку на сборщик мусора при копировании объектов на каждое изменение. Даже просто пройти по всем редьюсерам с помощью операции сравнения строк — это очень дорого, намного дороже, чем работа с объектами по ссылке.
В MobX вы не думаете о производительности, потому что она у вас из коробки.
Почему не useContext
Потому что useContext не дает производительности из коробки, вам дополнительно нужно будет обмазаться useMemo и useCallback.
Посмотрите наглядный пример от пользователя @MaZaAa — alert() выскочит только один раз, при первом рендере.
Так как MobX переопределяет shouldComponentUpdate, вам не нужно за этим следить вручную — перерендерится только то, что надо, а не всё дерево.
Ненастоящие минусы MobX
1. Декораторы еще не в стандарте, но, во-первых, можно писать без декораторов, во-вторых, можно писать на TypeScript.
2. Пятый MobX не поддерживает IE11, потому что использует ES6 Proxy, который сложно полифилить. Для нас это не было проблемой, так как мы не поддерживаем браузер семилетней давности. Но если вы его по каким-то причинам поддерживаете (хотя уже сам Microsoft перестает это делать), можно использовать MobX 4 версии (UPD: или MobX 6).
3. Redux в свое время многих подкупил своими дев-тулзами. В MobX с этим тоже нет проблем, можно использовать mobx-devtools или mobx-remotedev.
4. Для Server-side рендеринга обычно на сервере формируют стейт, сериализуют его в JSON и кладут в window.__PRELOADED_STATE__. К сожалению, MobX никак вас не ограничивает в том, что вы можете положить в стейт. Там могут быть циклические данные и другие структуры, которые не могут быть однозначно представлены в JSON.
Но в целом, если вы не храните подобные структуры в стейте, то SSR с MobX — давно решенная проблема, например, в том же Nextjs с помощью useStaticRendering.
5. Мне непонятен этот аргумент, но часто можно слышать, что в MobX слишком много магии. Если не разобраться, как устроена какая-то технология, то, конечно, она покажется магией. MobX — это просто FRP, только вам не нужно вручную подписываться на наблюдаемые объекты. MobX делает это за вас и прозрачно. Он наблюдает, к каким данным вы обращаетесь, подписывается на них и так строит объектный граф.
Создать свой аналог MobX можно за полчаса.
Настоящие минусы MobX и как с ними бороться
Единственный реальный минус MobX — он дает вам слишком много свободы в том, как структурировать код, хранить и обрабатывать данные. Для больших команд и крупных проектов это может быть проблемой, поэтому расскажу, как мы боролись с излишней свободой.
Не мутировать модель в представлении
Вернемся к нашему примеру со счетчиком:
Мы могли бы написать его вот так, то есть мы можем мутировать состояние прямо во вьюхе:
Мы решили ни в коем случае так не писать, так как еще наши деды учили разделению ответственности и MVC.
Чтобы запретить мутировать состояние вне хранилища, можно использовать флаг enforceActions в настройках MobX. Но он будет предупреждать вас только в рантайме и создавать проблемы с Promise.
Второй вариант — помечать поля объекта как private. Но в этом случае на каждое приватное поле вам придется создать геттер.
Возможно, вам будет достаточно просто внутри команды договориться мутировать поля исключительно через экшены и следить за этим в код-ревью.
Не наследовать стор от стора
MobX позволяет создавать вычисляемые поля от вычисляемых полей другого стора. Одна наблюдаемая переменная может обновлять другую, которая загружает данные от третьей. Чтобы избежать этой каши, мы решили не наследовать стор от стора.
В классической MVC-архитектуре контроллер получает команды от пользователя, работает с моделью и оповещает view об изменениях. Если нужно разделить какую-то логику между контроллерами, вы создаете сервисный слой.
Мы решили придерживаться этого же пути. Если вам нужны данные одного стора в другом, вы создаете третий стор, в котором объединяется логика двух других. Потому что с точки зрения бизнес-логики это новый доменный объект.
Сервисы предоставляют данные, инкапсулируют бизнес-логику и дают возможность компонентам общаться друг с другом. Так работает Ember и Angular, но к ним мы еще вернемся.
Не внедрять модель через провайдера
Документация MobX учит подключать сторы таким образом:
Мы же решили просто подключать через импорты:
Импортируем Store напрямую и получаем все бонусы от IDE, такие как автокомплит и статический тайпчекинг.
На Хабре есть огромный тред по этому поводу, там десятки сторонников и противников подобного подхода, не буду повторяться.
Почему не Vue/Angular/Ember
У команды был большой опыт с React, поэтому мы решили не менять фреймворк. Но в целом — связка React+MobX с наблюдаемыми и вычисляемыми полями может напомнить то, как работает Vue.js.
С ограничениями, которые мы наложили на MobX, организация данных стала похожа на Angular. Вот пример из официальной документации:
Получается, что весь цивилизованный энтерпрайз-мир так разрабатывает приложения. Все данные одной доменной области в одном месте. В этом же месте методы для изменения этих данных — всё максимально прозрачно.
Redux кичится тем, что имеет один источник правды, хотя по факту — это просто глобальная переменная. В сложных проектах вам, скорее, нужно много объектов, каждый из которых отвечает за свою доменную область, чем куча хлама в глобальной переменной.
Не надо думать, что размазав чудовищный редаксовский код по пяти файлам, вы тем самым повышаете его читабельность и ремонтопригодность.
Выводы
Мы выбрали MobX, потому что это простая и эффективная библиотека, которая позволяет аккуратно упорядочить сложные модели предметной области в классы.
В свое время Redux победил, потому что был разумной альтернативой императивному jQuery. Но необязательно страдать всю жизнь, пора двигаться дальше.
React + mobx путь с нуля. Mobx + react, взгляд со стороны
Кроме того, для работы с декораторами и трансформации свойств классов нам потребуются babel плагины babel-plugin-transform-class-properties и babel-plugin-transform-decorators-legacy:
У нас есть компонента Menu, давайте продолжим работу с ней. У панели будет два состояния «открыта/закрыта», а управлять состоянием будем с помощью mobx.
1. Первым делом нам нужно определить состояние и сделать его наблюдаемым посредством добавления декоратора @observable. Состояние может быть представлено любой структурой данных: объектами, массивами, классами и прочими. Создадим хранилище для меню (menu-store.js) в директории stores.
Стор представляет собой ES6 class с единственным свойством show. Мы повесили на него декоратор @observable, тем самым сказали mobx-у наблюдать за ним. Show — это состояние нашей панели, которое мы будем менять.
2. Создать представление, реагирующее на изменение состояния. Хорошо, что у нас уже оно есть, это component/menu/index.js. Теперь, когда состояние будет изменяться, наше меню будет автоматически перересовываться, при этом mobx найдет кротчайший путь для обновления представления. Что бы это произошло, нужно обернуть функцию, описывающую react компонент, в observer.
В любом react приложении нам понадобится утилита classnames для работы с className. Раньше она входила в пакет react-а, но теперь ставится отдельно:
c её помощью можно склеивать имена классов, используя различные условия, незаменимая вещь.
Видно, что мы добавляем класс «active», если значение состояние меню show === true. Если в конструкторе хранилища поменять состояние на this.show = true, то у панели появится «active» класс.
3. Осталось изменить состояние. Добавим событие click для «гамбургера» в
Note: По дефолту мы экспортируем хранилище как инстанс синглтона, также экспортируется и класс напрямую, так как он тоже может понадобиться, например, для тестов.
Для наглядности добавим стили:
И проверим, что по клику на иконку, наша панель открывается и закрывается. Мы написали минимальный mobx store для управления состоянием панели. Давайте немного нарастим мяса и попробуем управлять панелью из другого компонента. Нам потребуются дополнительные методы для открытия и закрытия панели:
Можно заметить, что мы добавили computed и action декораторы, они обязательны только в strict mode (по умолчанию отключено). Computed значения будут автоматически пересчитаны при изменении соответствующих данных. Рекомендуется использовать action, это поможет лучше структурировать приложение и оптимизировать производительность. Как видно, первым аргументом мы задаём расширенное название производимого действия. Теперь при деббаге мы сможем наблюдать, какой метод был вызван и как менялось состояние.
Note: При разработке удобно использовать расширения хрома для mobx и react, а так же react-mobx devtools
Создадим еще один компонент
Внутри пара кнопок, которые будут открывать и закрывать панель. Этот компонент добавим на Home страницу. Должно получиться следующее:
В браузере это будет выглядеть так:
Теперь мы можем управлять состоянием панели не только из самой панели, но и из другого компонента.
Note: если несколько раз произвести одно и тоже действие, например, нажать кнопку «close left panel», то в деббагере можно видеть, что экшен срабатывает, но никакой реакции не происходит. Это значит, что mobx не перересовывает компонент, так как состояние не изменилось и нам не нужно писать «лишний» код, как для pure react компонент.
Осталось немного причесать наш подход, работать со сторами приятно, но разбрасывать импорты хранилищ по всему проекту некрасиво. В mobx-react для таких целей появился Provider (см. Provider and inject) — компонент, который позволяет передавать сторы (и не только) потомкам, используя react context. Для этого обернем корневой компонент app.js в Provider:
Тут же импортируем все сторы (у нас один) и передаём их провайдеру через props. Так как провайдер работает с контекстом, то сторы будут доступны в любом дочернем компоненте. Также разобьем menu.js компонент на два, чтобы получился «глупый» и «умный» компонент.
«Глупый» нам не интересен, так как это обычный stateless компонент, который получает через props данные о том открыта или закрыта панель и колбэк для переключения.
Гораздо интереснее посмотреть на его враппер: мы видим тут HOC, где мы инжектим необходимые сторы, в нашем случае «leftMenuStore», в качестве компонента мы передаем наш «глупый компонент», обернутый в observer. Так как мы приинжектили leftMenuStore, то хранилище теперь доступно через props.
практически тоже самое мы проделываем с left-panel-controller:
С той лишь разницей, что тут мы не используем observer, так как для этого компонента перерисовавать ничего не требуется, от хранилища нам нужны лишь методы openLeftPanel() и closeLeftPanel().
Note: я использую displayName для задания имени компоненту, это удобно для деббага:
Это все просто, теперь давайте получим данные с сервера, пусть это будет список пользователей с чекбоксами.
Идем на сервер и добавляем роут «/users» для получения пользователей:
Нарочно добавим задержку, чтобы проверить, что приложение работает корректно даже с большим интервалом ответа сервера.
Далее нам понадобится
Тут описан класс User со свойством user. В mobx есть observable.map тип данных, он как раз подойдет нам для описания user-а. Грубо говоря, мы получаем наблюдаемый объект, причем, наблюдать можно за изменением конкретного поля. Также становятся доступны getter, setter и прочие вспомогательные методы. Например, в конструкторе с помощью «merge», мы легко можем скопировать поля из userData в user. Это очень удобно, если объект содержит много полей. Также напишем один action для переключения состояния пользователя и вычисляемое значения для получения информации о пользователе.
Ниже описан сам стор, в котором наблюдаемый являемся массив пользователей. В конструкторе мы дергаем метод для получения пользователей с сервера и через action putUsers заполняем пустой массив пользователями. Напоследок, добавим метод, который возвращает вычисляемое количество чекнутых пользователей.
Note: autorun выполняет функцию автоматически, если наблюдаемое значение было изменено. Для примера, тут выводится все пользователи в консоль. Если попробовать достать пользователей методом «getUsers()», то можно заметить, что тип возвращаемых данных не Array, а ObservableArray. Для конвертации observable объектов в javascript структуру, используем toJS().
В app.js не забудем дописать новый user-store, чтобы потомки могли им пользоваться.
Добавим react компоненты в директорию components:
Тут уже привычная нам обертка, передаем массив юзеров и количество чекнутых пользователей через props.
Показываем список пользователей и информацию по их количеству. Передаём «toggle()» метод стора через props.
Рендерим одного пользователя.
Добавляем стили и цепляем готовый компонент на Home страницу. Все готово(github), можно поиграть с чекбоксами и убедиться, что все методы работают.
В итоге мы увидели как работает mobx в связке с react-ом, учитывая все возможности mobx, можно предположить, что такое решение имеет право на жизнь. Mobx прекрасно справляется с обязанностью менеджера состояний для react приложений и предоставляет богатый функционал для реализации.