Ссылка на первую часть статьи: «Проблемные места Redux». В этой части я опишу, приблизительно какую архитектуру использую в своих проектах. MobX взят, так как он довольно простой и удобный, из коробки есть готовая реализация паттерна Observer, автоматическая мемоизация и автоматическое обновление компонентов при изменении состояния хранилища. Я много раз читал, как кто-то попробовал MobX, у него код получился запутанным с не контролируемыми изменениями, после чего он продолжил писать на Redux. Для MobX нет рекомендованной архитектуры. Но при использовании и соблюдении в MobX строгой и однообразной (имеется ввиду одинаковой в различных участках проекта) архитектуры, можно получить понятный код с контролируемыми изменениями в сколь угодно большом проекте. Я опишу один из вариантов, как этого добиться. Отмечу, что последние 5 лет я работал только с REST-подобными API, поэтому код в статье заточен под работу с REST API. Будет описано разделение на слои. Моя цель - не показать, как правильно реализовать каждый слой в приложении, а показать, что количество составляющих каждого слоя может быть гораздо меньше, чем в Redux, а их взаимодействие проще. Подход будет рассмотрен на примере простого списка дел. Код в примерах будет приведен не полностью. Полный пример кода находится в github и в codesanbox. Структура папок проекта по большей части - Folder-by-feature. Если у вас проекте есть одна общая папка вроде ducks/stores, где находятся все редьюсеры/actions/stores, то структура папок у вас вряд ли хорошо масштабируется и вам стоит обратить внимание на структуру в моем примере. Суть такая, что файлы, которые относятся к конкретной feature/странице, стоит располагать рядом с ней, а не размещать в разных участках проекта. Содержание Пример слоя сторов на MobX Сервисный слой (API и другие сервисы) Пример слоя controller (альтернатива middleware в моем примере) Пример инициализации сторов, api и контроллеров Пример использования стора и контроллера в компонентах Схема архитектуры. Преимущества и недостатки. Как и в Redux, этот слой не зависит от других слоев. Используется несколько сторов - один для работы со списком, другой для работы с формой, третий для работы с параметрами поиска (фильтрация, сортировка, пагинация). В статье приведен пример только одного стора. Не обязательно так разделять стор для каждой feature пока не будет видно, что от разделения будет польза. Без разделения, в моем случае объявление стора выглядело бы так: "BaseStore<TListItem, TEditItem, TSearchParams>", что как минимум затрудняет читабельность. Для сторов и некоторых других программных сущностей (API, middleware) я буду использовать базовые классы. На github можно заметить, что у меня в папке todos (то есть в папке реализации конкретной страницы/feature) практически нет кода в файлах api.js, controllers.js, stores.js, т.к. общий код вынесен в базовые классы. Конечно, при разрастании кодовой базы, ситуация может измениться. В рассматриваемом подходе не обязательно использовать базовые классы и наследоваться от них, т.к. они могут не подходить для всех случаев. К тому же можно не использовать классы, а делать отдельные функции. Классы здесь удобны тем, что позволяют стандартным механизмом избавиться от дублирования функций с одинаковым кодом и позволяют задать в конструкторе общие зависимости для всех методов. В общем, менее важно, как слои реализованы внутри. Самое главное, что слои API, контроллеров  (о них будет рассказано позже), сторов и компонентов отделены друг от друга. Далее стор для списка Todo. Пока-что нет необходимости создавать уникальных методов для функционала списка, поэтому вместо наследования можно воспользоваться обобщенным базовым классом BaseListStire<T>. Чтобы избежать дублирования логики и не засорять код контроллеров, в отдельный слой вынесен код для взаимодействия с сервером. В примере используется библиотека axios. Данный слой ничего не знает о других слоях. Он ни в коем случае не должен изменять стор или читать из него. В Redux ничего не сказано про этой слой, но многие создают его, как и я. В моем примере общий код для всех запросов и инициализация axios вынесены в отдельный сервис. Т.к. пример маленький, пользы от этого не видно. Но в большом проекте это с большой вероятностью пригодится в будущем. Например, если потребуется задать общие заголовки или преобразовывать формат всех данных перед отправкой или при получении.  В статье в качестве сервисного слоя приведены только api сервисы. Но могут быть и другие сервисы.  Далее базовый класс с методами для работы с конкретным маршрутом, использующий apiService. Этот класс тоже часть сервисного слоя. В своем коде вместо middleware я буду использовать термин из MVC - Controller. В Redux есть возможность объединять middlewares в цепочки. Т.к. это далеко не всегда нужно, я не стал без необходимости усложнять и не реализовывал у себя в контроллерах объединение действий в цепочки. К тому же, промежуточное ПО можно разместить при получении данных с сервера или при передачи данных из API в контроллер. Так оно будет располагаться в зависимости от своего назначения, а не вперемешку. Основное назначение контроллера в данном подходе - быть посредником между api, стором и компонентом. То есть вызываться через компонент, вызывать api и методы обновления стора, а также содержать в методах логику, для выбора, какой api метод и стор использовать. Не стоит переусложнять бизнес-логикой контроллер. Общую для всех контроллеров бизнес-логику лучше выносить отдельно, как сделано в случае api. В идеале в больших проектах стоит стремиться к активной модели контроллера, но с другой стороны, в небольших проектах это может быть ненужным усложнением. Контроллер используются только в слое View (компоненты) и в других контроллерах.  Этот слой зависим от слоя сторов и слоя API. Я использую базовый класс, в котором находятся несколько общих методов для обновления стора и для получения данных с сервера типичными CRUD методами. Далее пример создания экземпляров классов сторов, api и контроллеров. Чтобы передавать экземпляры сторов и контроллеров в компоненты, а также не мокать импортируемый функционал в тестах, в этом примере я воспользовался контекстом. В данном примере в использовании контекста нет необходимости. Можно было бы экспортировать объект со сторами и объект с контроллерами напрямую, а не через context. Честно говоря, я пока не вижу ситуации, где один из подходов работает, а другой нет. Писать тесты с подменой сторов и контроллеров можно и без контекста. Например, в случае использования Jest, если у вас есть файл "srс/pageA/myStore.js", то в папке pageA надо создать папку __mocks__  и создать в ней файл myStore.js для использования его в тестах вместо оригинального файла. То есть расположить по такому пути: "srс/pageA/__mocks__ /myStore.js". А в файле с тестом (например: "srс/pageA/__tests__ /MyComponent.js") после import-ов достаточно написать "jest.mock('../myStore');". Получилась архитектура со следующими составляющими:  Service (сервисы для работы с API, а также сервисы, в которые вынесен общий функционал для контроллеров)  Controller (для связи между API, сторами и компонентами)  Store (для работы с общими данными (состоянием) приложения)  View (компоненты) Сравнение ее составляющих с Redux: Redux Services (опцио-нально) Middle-ware's  Action creators Actions Reducers Selectors Compo-nents мой подход Services Controllers с функциями-действиями Stores с функциями, аналогичными  сеттерам и геттерам. Compo-nents Вместо 7-ми видов сущностей, которые нужно постоянно создавать, получилось 4-ре. Масштабируемость, на мой взгляд, примерно такая же. Ниже изображены 2 схемы:1) Схема зависимостей, отображающая, какие сущности использует такая-то сущность. 2) Схема потока данных, отображающая из каких сущностей в какие передаются данные. Под "get" имеется ввиду, что сущность сама запрашивают данные, а под "pass" имеется ввиду, что другая сущность является инициатором передачи данных. Получившееся похоже на вариацию MV* с добавление стора. Вместо View Model как в MVVM, здесь используется стор, остальное же - обычное MVC. Подобную архитектуру я успешно использую с 2016 года. Я далеко не сразу пришел к тому виду, который описан в статье. Что-то улучшил после предложений других разработчиков в команде. Что-то сам решил изменить. Что-то еще в будущем буду менять. Рассмотрю несколько ситуаций с использованием описанной архитектуры. 1. Что делать, если один стор должен использовать данные другого стора?Я стараюсь избегать прямой связи одного стора с другим. Вместо этого я передаю эту обязанность  контроллеру. В действии контроллера, которое должно обновить первый стор, считываю данные из второго стора и передаю их вместе с остальными данными в первый стор. 2. Что делать, когда наблюдаемые данные одного стора зависят от наблюдаемых данных другого стора и происходит обновление первого стора?Я стараюсь избегать таких цепочек обновлений и выношу вычисления в контроллер. То есть из сторов считываю необходимые данные, обрабатываю их, и затем передаю их сторам, использующим эти связанные данные.Здесь я ошибся. Спасибо @Alexandroppolusза поправку.  3. (updated) Если нужно запретить возможность обновление стора из компонента, чтобы случайно не нарушить архитектуру, то можно вынести отдельно функционал записи в стор и не передавать этот функционал в компонент. Спасибо @dani_jug за указание этого момента комментарии.   4. Если в нескольких компонентах нужно вычислить и подписаться на значение, состоящее из данных нескольких сторов, можно вынести вычисление этого значения в отдельную функцию. Можно сделать custom hook или воспользоваться функцией MobX - computed. Спасибо @DmitryKazakov8 за его комментарий к предыдущей части статьи! После него решил добавить этот пункт. 5. Уменьшение бойлерплейта. В описанном подходе, если для множества страниц приходиться писать однотипный функционал, можно написать обертку для инициализации и связывания экземпляров контроллеров, сторов, api. Для примера, черновая версия у меня есть в отдельной ветке:wrapFeature.ts (создает экземпляры переданных, либо базовых api, stores, controller для одной feature и возвращает их)Пример использования Важно не переусердствовать и не писать сложные универсальные решения. Если вы не видите необходимости в использовании context или предпочитаете использовать import/export, то можно еще немного уменьшить количество кода. Преимущества: Сторы получаются менее раздуты, чем ViewModel-и в MVVM. Функции изменения состояния в сторе такие же простые, как редьюсер с одним action.  Нет зависимости cтора от других слоев.  Легче избавиться от шаблонного кода (избавиться от необходимости создавать стор и контроллер для каждой страницы). Если будет решено заменить менеджер состояний, то не нужно (или почти не нужно) будет переделывать код действий в контроллерах. Аналогично в ситуации, когда в следующем похожем проекте захотелось воспользоваться готовым функционалом из текущего проекта. Но это довольно редкие ситуации. Недостатки: Необходимо создавать больше программных сущностей, что, как минимум, для небольших проектов явное усложнение.  У компонента в 2 раза больше зависимостей (не только сторы, но и контроллеры). На данный момент вся информация об описанной архитектуре ограничена этой статьей и примером кода. Frontend developer (React, MobX)