В этой статье я бы хотел подискутировать о том, насколько хорошо паттерн MVVM подходит для разработки приложений на React. Вместе этим, я собираюсь описать какие преимущества могут быть при разработке с использованием MobX и паттерна MVVM в сравнении с Redux. Запаситесь кофе, это будет долгое чтиво. Вообще если проанализировать статьи по сравнению MobX и Redux, можно заметить несколько часто повторяющихся тезисов: "MobX проще в изучении, чем Redux" "MobX более производительнее Redux" "В MobX нужно писать меньше кода, чем в Redux" Однако, "Масштабирование на Redux проще, чем на MobX", а потому "MobX подходит для небольших приложений, а Redux для больших" "Процесс дебага на Redux проще, чем на MobX" "Сообщество Redux обширнее, чем у MobX" "В MobX дается слишком много свободы" MobX vs Redux: A Comparison Redux vs MobX. Everything You Need to Know Difference Between Mobx and Redux MobX or Redux: Which is Better For React State Management? Почему мы выбрали MobX, а не Redux, и как его использовать эффективнее На самом деле первые 4 статьи написаны будто под копирку и подобных статей очень много. Глупо отрицать, что в сравнении с Redux сообщество MobX не такое большое - одних только скачивай на npmjs.com у Redux больше в 8 раз чем у MobX. Однако, с остальными минусами я с трудом могу согласиться. Проблемы с дебагом, как мне кажется, либо являются исключительно субъективными, либо появляются в следствие плохой архитектуры приложения. Сам же я, например, никогда проблем с дебагом MobX не испытывал. Поинт про масштабирование в моих глазах тоже выглядит странно. Чем больше становится приложение на Redux, чем больше в нем селекторов, больше логики в редьюсерах, больше мидлварей, тем медленней он становится. Как бы не был оптимизирован код на Redux, при масштабировании производительность будет обязательно падать. MobX же в плане производительности может расширяться довольно безболезненно. Поэтому я предполагаю, что авторы таких статей говорят о проблемах в масштабировании в контексте написания нового кода. В таком случае это снова является проблемой плохой архитектуры. И вот мы добрались до сладкого. Я не просто так выделил проблему излишней свободы в MobX. Ведь она вполне реальна. Создавай сторы как угодно; используй их в компоненте, предварительно импортируя, используй их с помощью инъекции или создавай их в компоненте. Подходов к использованию MobX в React можно придумать невероятно много. И MobX даже не пытается рекомендовать какой-то один "хороший" подход. А в совокупности с тем, что его "легко изучить", эта проблема только усугубляется, так как, быстро выучив основные концепции MobX, некоторые разработчики считают, что они определенно точно знают, как нужно продумывать архитектуру. Но все-таки проблема архитектуры приложения не связана с MobX напрямую. Странно обвинять пистолет, если ты сам выстрелил себе в ногу. Проблема в том, что разработчики не выбрали какой-то определенный подход к использованию этого инструмента. А ведь прорабатывать подход с нуля вовсе не обязательно - существующие концепции неплохо справляются с задачей по проработке архитектуры. И как вы наверняка догадались, в рамках данной статьи я попытаюсь показать, как паттерн MVVM может в достаточной степени сформировать ту строгость, о недостатке которой говорится в подобных статьях. Для этой статьи я написал небольшую библиотеку, с помощью которой можно было бы использовать паттерн MVVM. Она состоит буквально из 3 функций и 2 классов. Далее по ходу статьи я буду часто к ней обращаться. Основное преимущество паттерна MVVM (Model View ViewModel) заключается в разделении разработки графического интерфейса и логики.  И я говорю о полном делении логики и отображения. В подходе MVVM ваши умные компоненты могут состоять исключительно из JSX кода. Таким коротким примером я всего лишь хотел вас заинтересовать. Но давайте не забегать вперед, и сначала обозначим, как можно использовать MVVM в React-приложениях. Невозможно описать все плюсы MVVM, не рассказав об особенностях реализации этого паттерна в React-приложениях. Для себя я выделил всего 4 правила в реализации MVVM: Каждая вьюмодель должна быть привязана ко вью и, следовательно, при удалении вью из разметки вьюмодель должна "умирать"; Каждая вьюмодель может (и зачастую будет) являться стором; Этот стор может использоваться не только в компоненте, который создал вьюмодель (т.е. внутри вью), но и во всех дочерних компонентах вью; Вью и вьюмодели знают о существовании друг друга. В чем заключается осведомленность вью и вьюмодели друг о друге? В React приложении вью создаст сам React. А вот за создание вьюмодели уже будет ответственен вью. В свою очередь вьюмодель знает о пропсах вью и способна обрабатывать различную логику на разных этапах жизненного цикла вью. Наверняка, вы задались вопросом: "Но почему вьюмодель должна умирать, когда вью уходит из разметки?". Ответ довольно прост. Если компонент более не находится в виртуальном DOM'е, с большой вероятностью необходимости в хранении данных для отображения этого компонента нет. Эта оптимизация позволяет освобождать память приложения, когда некоторые куски данных не используются. И это лишь одно из немногих преимуществ. Третье "правило" моей реализации говорит, что вьюмодель может использоваться на любой глубине внутри вью. Понятно, что речь идет о Context API. И его вполне можно использовать вместе с паттерном MVVM.  По мне так тут прослеживается довольно четкая аналогия с Redux, который исключительно на Context API и существует. Однако, между подходом Redux и MobX с MVVM есть существенная разница. Каждый узел в графе на схеме является React-компонентом. В подходе Redux создается один стор. И этот стор доступен во всем приложении. В подходе MVVM стор создается только для вью. И в данном случае создается всего 3 стора. При этом синий стор и компоненты, его использующие, не знают о существовании желтого и красного стора, они никак не могут прочитать их данные или как-то на них повлиять. В отличии от Redux, в MVVM появляется реальная абстракция данных. Например, данные, необходимые одной странице, могут быть видимы только внутри этой страницы, а то, что нужно модальному окну, будет видно только ему. Сейчас вы могли увидеть некоторое противоречие. Я говорил, что вьюмодель должна быть доступна для любых детей вью, а на схеме ясно видно, что красные узлы имеют собственную вью модель, но никак не "желтую". На такой случай было введено дополнительное понятие - родительская вьюмодель. Родительской вьюмоделью для "красной" будет являться "желтая".  Но интересен также тот факт, что родительской вьюмоделью может быть даже интерфейс. Если некоторое вью может использоваться внутри разных вью, вьюмодели которых реализуют некоторый единый интерфейс, то в качестве типа родительской вьюмодели можно его и указать.   Звучит, наверное, сложно. Но на практике это не так. Например, допустим в проекте есть некоторый виджет, который может появляться на нескольких страницах, и при этом зависеть одинаковым образом от этих страниц. У всех страниц будут свои вью модели, но правила отображения виджета будут одни для каждой страницы. И эти правила как раз можно было бы обозначить интерфейсом родительской вьюмодели. Как я написал выше, в MVVM сепарация логики и отображения может выйти на новый уровень. Мне невероятно сильно нравится концепция того, что умные компоненты могут состоять исключительно из JSX кода. По мне так это сильно упрощает анализ React компонент. Начнем с простого. Обработчики событий по типу onClick, onInput и т.п. по определению MVVM можно определять во вьюмодели. В самом первом примере во вьюмодели я создал функцию increase, которую далее вызвал в компоненте. Но мне ничего не мешало бы назвать функцию как onClick, тогда в компоненте можно было бы использовать её напрямую  Кстати, у такого объявления обработчиков есть приятный эффект - все функции внутри вьюмодели, можно сказать, мемоизированы, т.е. они не меняются от рендера к рендеру вью. А значит не нужно беспокоиться об использовании useCallback и других подобных хуков при создании обработчиков во вьюмодели. Даже если обработчики событий как-то зависят от пропсов вью, их все равно можно объявлять внутри вьюмодели, т.к. вьюмодель знает о пропсах вью.  А ещё мне показалось логичным сделать поле viewProps наблюдаемым (observable). Таким образом можно создавать реакции на изменение определенных пропсов. И такие реакции по мне гораздо проще читать нежели реакции, создаваемые в useEffect. Хотя, вероятно, это субъективное мнение.   Также из-за того, что viewProps является наблюдаемым, есть дополнительный приятный эффект. Если вложенные компоненты будут каким-то образом зависеть от пропсов своего вью, то при обновлении пропсов вью автоматически они будут сами обновляться. Про хуки я уже начал говорить в прошлом блоке. Создавать реакции на определенные объекты через useEffect больше необходимости нет. Но useEffeсt нужен не только для реакций - его можно также использовать для обработки состояний жизненного цикла компонента. Например, при монтировании, размонтировании и обновлении. И все эти этапы жизненного цикла вполне могут обрабатываться внутри вьюмодели.  Это небольшой, но очень приятный бонус. В умных компонентах количество пропсов может свестись к минимуму. В моем прошлом проекте в среднем у каждого умного компонента было не больше 3 пропсов. Достигается это за счет того, что в MVVM нет необходимости в дриллинге пропсов. Дочерние компоненты вью могут напрямую обращаться к своей вьюмодели. Если они или их вью зависят от состояния вышестоящего вью, они могут обратиться к полю parent. Если есть зависимость от пропсов вью, то можно обратиться к полю viewProps. В остальных случаях, конечно, передавать пропсы все ещё нужно напрямую, но даже такая небольшая возможность позволяет в разы сократить количество передаваемых пропсов. Что влияет не только на простоту анализа кода, но и на производительность приложения, т.к. мемоизированному компоненту нужно проверять меньше пропсов при обновлении. Чтобы вы ещё лучше осознали всю прелесть MobX с MVVM, я попробую сравнить эту связку с Redux. Посмотрите на пример ниже. Я расписал тот же код, что в примере по хукам для MVVM, но написанный на Redux.  В этом примере видна сила полноценного деления логики и отображения. В примере с Redux компонент ответственен за обновление состояния. Экшены объявлены где-то в другом месте, но они должны быть использованы внутри компонента. В примере же с MVVM вью не вызывает хуки и никаким образом не обрабатывает данные. Он их только использует. Что ещё интересно, так это то, что кода в обоих примерах одинаковое количество. На самом деле в MVVM его чуть больше, но кода в конструкторе можно избежать (об этом ниже). Но при этом в Redux нужно ещё создать слайс, а следовательно в Redux кода приходится писать больше. Вообще количество кода - это отдельный приятный бонус в сравнении MobX и Redux. Я думаю, разработчики, использующие Redux, часто задумываются о том, как же много повторяющегося кода приходится им писать. Redux Toolkit улучшил, конечно, ситуацию, но полноценно избавиться от повторяющегося кода не удалось. Нужно вызывать хук, чтобы получить значение стора; нужно вызывать хук, чтобы получить функцию dispose; нужно указывать специфичные имена для экшенов и/или слайсов; и т.п. Ну и типизация. Redux Toolkit и её довольно сильно упростил. Но по мне так она все ещё выглядит костыльно. Получение типа стора по результату функции, возвращающей его; создание редьюсера с определенным типом с помощью дополнительной функции с дженериком; или та же необходимость прокидывать тип стора в каждый вызов useSelector. Не могу сказать, что это приятные для использования решения. В примерах же с MobX задумываться о типизации нужно редко. Я создал класс вьюмодели а затем использую его объект. И TypeScript сам расставит всю типизацию.  Вместе с этим, в Redux нужно обязательно думать о мемоизации. Количество useMemo и useCallback на квадратный сантиметр кода в Redux может разительно отличаться от этого количества в MobX с MVVM. Ну и пропсы. На моей практике в Redux довольно часто встречается дриллинг пропсов, хотя бы в рамках вложенности в 1-2 слоя. А, как я написал выше, в MVVM таких проблем меньше. Согласитесь, MVVM уже выглядит довольно-таки неплохо. Но я решил пойти ещё дальше и немного "прокачать" свою реализацию MVVM. Документация MobX говорит, что вы всегда должны вызывать dispose реакций. Это правило помогает избегать проблем с возникновением утечек памяти. Но следовать этому правилу не очень удобно. Но не в подходе MVVM. Итак, когда реакции, созданные во вьюмодели, перестают быть нам нужны? Учитывая, что вьюмодель должна умирать после размонтирования компонента, то в большинстве случаев в момент, когда вью размонтируется. Поэтому можно добавить автоматический вызов диспозеров при размонтировании вью. В таком случае реакции можно создавать при помощи небольших функций-надстроек внутри вьюмодели. Интерфейс и типизация при этом у этих функций не будет отличаться от их аналогов в MobX.  Подход MVVM можно гибко подстроить под себя. Например, моя реализация позволяет настроить то, как создается вьюмодель, а также позволяет назначить компонент-обертку для всех вью и чайлдвью. И эти 2 настройки гораздо мощнее, чем вам может показаться. Разумеется, эти настройки можно использовать в том числе и для дебага, но это не основное их назначение. Например, вы можете сделать вызов функции makeObservable автоматическим, чтобы не вызывать у каждой вьюмодели эту функцию в отдельности.  Вы можете создавать вьюмодель таким образом, чтобы в ваш проект можно было добавить паттерн DI (Dependecy Injection). И по мне так это очень приятный бонус. Благодаря DI вы можете создавать маленькие независимые сторы, которые могут использоваться в разных частях вашего проекта. И вместо зависимости от виртуального дерева, с DI вы можете выстраивать зависимости абсолютно свободным образом.  Возможность конфигурации обертки для каждого вью мне тоже показалось очень интересной идеей. Мне понравилась возможность в качестве обертки использования Error Boundary. В таком случае все ваши вью будут автоматически оборачиваться в Error Boundary и вам придется меньше думать об обработке ошибок при разработке. Вам может показаться, что это слегка излишне, но в реальности далеко не все компоненты будут вью, а лишь те, что содержат много логики.  Поздравляю, вы почти дошли до конца статьи. Но, нет, к сожалению MVVM - это не ультимативное решение всех проблем. MVVM позволяет решить многое. По мне так очень многое. Мне правда нравится этот паттерн. Но есть одна серьезная проблема. Бывают такие моменты, когда некоторые данные нужны в абсолютно разрозненных частях проекта. Снова граф React-компонет. Желтым цветом я пометил компоненты, которые зависят от одного набора данных. Если такие компоненты находятся относительно близко друг к другу, то вполне реально разместить данные где-нибудь в родительской вьюмодели. Однако, с увеличением глубины, количество используемых родителей может возрастать. Согласитесь, если придется получать данные через viewModel.parent.parent.parent.parent.data, то это уже совсем неудобно. Да и если хранить во вьюмодели данные, которые нужны парочке детей на огромной глубине внутри, то вьюмодель будет невероятно сильно разрастаться. В данной проблеме мы снова возвращаемся к тому, что нужно использовать некоторые независимые сторы, которые никак не привязаны к виртуальному дереву, а следовательно такие сторы никак не облагаются строгостью MVVM. Но решение есть. И я даже думаю, что вы наверняка уже догадались о чем я говорю. Паттерн DI может наложить ту самую строгость. И он довольно-таки хорошо ложится на использование совместно с MVVM. И вот MVVM + DI уже выглядит как полноценное решение для крупных проектов, которое позволит долго масштабировать проект. В этой статье я и так уже много написал, так что здесь в особенности DI я особо углубляться не буду. Однако, вы можете почитать о комбинации паттернов MVVM и DI в моей следующей статье. Спасибо за уделенное время. Не стесняйтесь делиться своим мнением в комментариях, с удовольствием почитаю их. А ещё, как я говорил, я подготовил крошечную библиотеку, буквально 300 строчек кода. Вы можете её использовать, если хотите поиграться с паттерном MVVM. Вот ссылки: npm, github, документация. А ещё можете посмотреть на пару примеров её использования.  Front-end Developer