В этой статье я опишу структурированный подход к использованию MobX, который может помочь упростить процесс разработки. Здесь не будет описываться код, только описание подхода к использованию. На код будут даваться ссылки. И я очень прошу вас посмотреть на примеры, которые я прикладываю. В них можно будет наглядно увидеть все плюсы описываемой архитектуры.
Также важно будет упомянуть, что для полного понимания описанного в статье, нужно быть знакомым с паттернами Observable/Observer, MVVM и DI.
О самой технологии MobX уже знает немало человек. Судя по данным npmjs.com, этот пакет в среднем скачивается около 850 тысяч раз в неделю. Его главным соперником можно считать библиотеку Redux, и судя по тем же данным, её в среднем скачивают в 8 раз чаще. Такую популярность Redux на фоне гораздо более удобного MobX мне сложно принять, так как я являюсь ярым сторонником этой библиотеки. Поэтому в этой статье я бы хотел описать, почему MobX настолько хорош и как можно сделать его ещё удобнее.
Вообще уже существует немало статей, описывающих плюсы MobX в сравнении с Redux, но для тех, кто с ними не знаком, сейчас я максимально кратко их опишу:
-
В MobX не нужно писать шаблонный код;
-
В MobX сторов может быть множество, а значит их можно логически разделять;
-
MobX гораздо проще для восприятия и изучения;
-
Типизация в MobX гораздо проще описывается и используется.
Но! Есть довольно большое «Но!». Мне не по душе подход самого MobX, который описывается для взаимодействия этой библиотеки с React. В примерах, которые описывают разработчики MobX предлагается создавать некоторый объект – стор, – в котором должна храниться обновляемая информация. Проблема в том, что это слишком размытое представление, не описывающее возможности более сложного взаимодействия, например, при вложенности компонент или при использовании одних сторов другими.
Уровень первый: Better MVVM
Начнем с терминологии. Гораздо логичнее использовать MobX, применяя паттерн MVVM. Формально, использование паттерна MVVM с MobX ничего нового не добавляет, только даёт названия сущностям системы. Моделью (Model) в таком подходе является любой JavaScript объект, а его описанием – его типизация. Представление (View) – это React-компонент. А модель представления (ViewModel) – некоторый класс, хранящий в себе observable поля, по факту являющийся стором.
На этом моменте заканчиваются все вводные. Далее я буду рассказывать о доработках, способных улучшить взаимодействие.
Взаимодействие View и ViewModel можно немного прокачать:
-
Сам объект ViewModel’и можно создавать в момент первой отрисовки View. Так, в конструкторе ViewModel можно писать код, который сработает перед монтированием View. К тому же в памяти компьютера не придется хранить неиспользуемую информацию.
-
Пропы, передаваемые во View можно передавать ViewModel’и. При этом удобно сделать поле, в котором хранятся пропы у ViewModel, observable, что позволит автоматически отслеживать их изменение.
-
При вложенности одной View в другой, можно передавать дочерней ViewModel ссылку на родительскую ViewModel.
-
При необходимости у ViewModel можно описать метод, запускаемый при монтировании и размонтировании View.
Для наглядности, давайте я опишу это на картинке

View1
инициализирует ViewModel1
, передавая ей свои пропы. View2
является дочерним элементом View1
(необязательно напрямую) в виртуальном DOM’е. View2
инициализирует ViewModel2
, передает ей ссылку на ViewModel1
и свои пропы.
View не обязательно должен быть observer-компонентом. Его главная задача проинициализировать объект его ViewModel’и, передать ей свои пропы и, если требуется, вызывать колбэки своего жизненного цикла.
ChildView
Ещё одна доработка по MVVM – введение понятия ChildView – некоторого компонента внутри View, который также имеет ссылку на ViewModel. ChildView не инициализирует новую ViewModel и не передаёт ей свои пропы. ChildView также не обязательно должен быть observer-компонентом.

В указанном примере у View
есть 3 ChildView
и каждая имеет ссылку на ViewModel. Из представленных ChildView observer-компонентом является только нижняя левая. Остальные используют статические поля и/или методы ViewModel.
Описание сущностей архитектуры в вакууме не имеет особого смысла. Поэтому я вам крайне рекомендую посмотреть на то, как эти сущности выглядят в коде: CodeSandbox / GitHub.
Уровень второй: Services
У описанного подхода все ещё могут быть свои минусы. Например, зачем хранить некоторую общую информацию для всего приложения в какой-то ViewModel’и? В таком случае логично сделать некоторый отдельный класс. Предлагаю называть такой класс сервисом.
В сервисе должна храниться общая информация, например, информация о пользователе – статус его авторизации и список разрешенных действий; или методы: создание всплывающего уведомления, переключение между страницами, и т.п. Сервис также может являться стором, то есть хранить observable поля.
Для связки сервисов и моделей представления идеально подходит паттерн DI. Для незнакомых с его концепцией, я рекомендую почитать про него отдельно. Но если вкратце DI хорош тем, что в классе достаточно в конструкторе указать тип нужного класса, а встроенные механизмы DI сами инициализируют нужный объект.
В JavaScript уже есть несколько готовых библиотек для реализации этого паттерна. В моей, которую я приложу в конце статьи, я использовал TSyringe.

Ограничений, накладываемых на сервисы, не очень много. Если в общем случае у ViewModel может быть ссылка на 1 другую ViewModel – родительскую, – то сервисов она может использовать сколько необходимо. Причем одни сервисы могут свободно использовать другие сервисы.
В реализациях DI часто есть деление на Singleton и Transient классы. И в общем случае я рекомендую делать ViewModel Transient классом, а сервис Singleton.
Если вам интересно посмотреть на работу View, ChildView и ViewModel совместно с сервисами и паттерном DI, можете взглянуть на этот пример: CodeSandbox / GitHub.
Уровень третий: Better Model
Как я писал выше любой JavaScript объект уже является моделью. В TypeScript по-хорошему у каждой такой модели должен быть определен тип. И этого уже достаточно.
Но на своем опыте я понял, что в формах часто имеется повторяющаяся логика по валидации полей и отслеживанию того, а было ли какое-то из полей изменено. Поэтому для таких форм я предлагаю использовать более прокаченную модель.
В таких моделях помимо информации о полях – их тип и, возможно, значения по умолчанию – можно хранить метаинформацию о полях. В дополнительных декораторах можно указывать, как нужно и нужно ли в принципе валидировать определенные поля; а также указывать изменение каких полей нужно отслеживать.
Поигравшись немного с этой концепцией, я описал сущность такой модели. У "прокаченных" моделей могут быть следующие плюсы:
-
Прокаченная модель автоматически отслеживает изменения своих полей. Причем, если при инициализации о модели поле field было равно пустой строке, затем стало равно любой другой строке, а затем снова пустой, модель будет говорить, что данные не менялись.
-
Модель способна отслеживать, какие поля в настоящее время не являются валидными. Причем в процессе валидации могут участвовать не только значения полей, но и весь объект в целом.
-
Прокаченная модель способна наследоваться от другой. При этом в дочерней модели можно перезаписывать метаинформацию родительской модели и/или создавать новые поля.
-
Полем модели может быть другая модель, массив моделей или словарь со значениями моделями. В таком случае у родительской модели есть механизм контролирования изменения и проверки валидации вложенных моделей.
Пример на работу с Model можно посмотреть по этим ссылкам: CodeSandbox / GitHub.
Резюмируя
В конце хотелось бы перечислить преимущества описанной архитектуры:
-
Используя данный подход к разработке можно структурировать подход к обработке состояния приложения. При этом сам подход использует исключительно существующие паттерны.
-
У этого подхода нет необходимости в написании шаблонного кода и нет никаких проблем с типизацией.
-
Данные можно логично разделить по нескольким классам-сторам, причем так, что общие (находящиеся в сервисах) можно будет использовать во всем проекте, а частные (находящиеся во ViewModel) можно будет использовать только в необходимых частях.
-
View могут состоять только из JSX кода, не используя ни одного хука.
-
Работа с формами сокращается до качественного описания моделей.
В дополнение могу ещё сказать, что разработчикам, знакомым с Vue, будет гораздо проще перейти на React с такой архитектурой, нежели с Redux, ведь MobX в формате MVVM имеет очень много схожих концепций.
Ссылки
-
Репозиторий с примерами.
Послесловие
В статье я занимался только описанием сущностей архитектуры, и того, как они должны между собой взаимодействовать. И хоть я приложил весьма, как мне кажется, полезные ссылки с кодом, своих комментариев я по нему не оставил. Поэтому чуть позже я напишу ещё 1-2 статьи, в которых я опишу полезные use-case’ы описанной архитектуры. А если в комментариях я увижу заинтересованность, я займусь этим как можно скорее.
Автор: Александрович Дмитрий