Приветствую, хабровчане! Читаю данный ресурс со времени основания, но время на написание статьи появилось только сейчас, а значит пора поделиться своим опытом с сообществом. Начинающим разработчикам, рассчитываю, статья поможет улучшить качество проектирования, а опытным выступит в качестве чек-листа, чтобы не забыть важные элементы на этапе архитектуры. Для нетерпеливых — итоговый репозиторий и демо. Допустим, вы устроились в «компанию мечты» — одну из бирж со свободным выбором технологий и ресурсами, чтобы сделать все «как надо». На данный момент все, что есть у компании — это Разработать SPA-приложение для торгового интерфейса, в котором можно: Задание достаточно краткое, что позволит сосредоточиться именно на архитектуре, а не написании больших объемов бизнес-функционала. Итогом первоначальных усилий должен стать логичный и продуманных код, позволяющий приступить непосредственно к реализации бизнес-логики. Так как в ТЗ от заказчика нет технических требований, пусть будут комфортные для разработки: Теперь время определить используемые инструменты и библиотеки. Я буду руководствоваться принципами разработки «под ключ» и KISS, то есть брать только те opensource библиотеки, для самостоятельной реализации которых потребовалось бы неадекватно много времени, включая время на обучение будущих коллег-разработчиков. Таким образом, из библиотек в итоговом файле приложения окажутся только React, MobX, HighStock, Lodash и Sentry. Считаю это оправданным, так как они имеют отличную документацию, быстродействие и знакомы многим разработчикам. Я предпочитаю разбивать зависимости в package.json на смысловые части, поэтому первым шагом после инициации git-репозитория сгруппирую все, что касается стиля кода в папке ./eslint-custom, указав в package.json: Обычный yarn install не проверит, изменились ли зависимости внутри eslint-custom, поэтому буду использовать yarn upd. В целом такая практика выглядит более универсальной, так как девопсам не придется менять рецепт деплоя, если разработчикам понадобится изменить метод установки пакетов. Файлом yarn.lock пользоваться нет смысла, так как все зависимости будут без «крышечек» semver (в виде "react": "16.8.6"). Опыт показал, что лучше вручную обновлять версии и тщательно их тестировать в рамках отдельных задач, чем полагаться на lock-файл, предоставляя авторам пакетов возможность сломать приложение минорным обновлением в любой момент (счастливчики, кто с этим не сталкивался). В пакете eslint-custom зависимости будут следующие: Чтобы связать три инструмента, понадобилось 5 вспомогательных пакетов (eslint-plugin-prettier, eslint-config-prettier, stylelint-prettier, stylelint-config-prettier, prettier-eslint) — такую цену приходится платить сегодня. Для максимального удобства не хватает только автоматической сортировки imports, но, к сожалению, этот плагин при переформатировании файла теряет строки. Конфигурационные файлы для всех инструментов будут в формате *.js (eslint.config.js, stylelint.config.js), чтобы на них самих работало форматирование кода. Правила пусть будут в формате *.yaml, разбитые по смысловым модулям. Полные версии конфигураций и правил — в репозитории. Осталось дописать команды в основной package.json... … и настроить свой IDE на применение форматирования при сохранении текущего файла. Для гарантии при создании коммита необходимо использовать git-хук, который будет проверять и форматировать все файлы проекта. Почему не только те, которые присутствуют в коммите? Для принципа коллективной ответственности за всю кодовую базу, чтобы ни у кого не было соблазна обойти валидацию. Для этого же при создании коммита все предупреждения линтера будут считаться ошибками с помощью --max-warnings=0. Снова воспользуюсь модульным подходом и вынесу все настройки Webpack и Babel в папку ./webpack-custom. Конфиг будет опираться на следующую структуру файлов: Грамотно настроенный сборщик предоставит: А также будет удобно конфигурироваться. Эту задачу решу с помощью двух *.env файлов-примеров: Таким образом, для запуска сборки нужно создать файл с названием .frontend.env и обязательным присутствием всех параметров. Данный подход решит сразу несколько проблем: не нужно делать раздельные конфигурационные файлы для Webpack и поддерживать их согласованность; локально можно настроить насколько это нужно определенному разработчику; девопсы при деплое будут лишь копировать файл для production-сборки (cp .frontend.env.prod.example .frontend.env), обогащая значениями из хранилища, соответственно frontend-разработчики имеют возможность управлять рецептом через переменные без задействования админов. Дополнительно можно будет сделать пример конфигурации для стендов (например, с source maps). Для отделения стилей в файлы при включенном CSS_EXTRACT буду использовать mini-css-extract-plugin — он позволяет использовать Hot Reloading. То есть, если при локальной разработке включить HOT_RELOAD и CSS_EXTRACT, то при
изменении файлов стилей будут перезагружаться только они — но, к сожалению, все, а не только измененный файл. С выключенным же CSS_EXTRACT обновляться будет только измененный стилевой модуль. HMR для работы с React Hooks включается достаточно стандартно: Текущая версия react-hot-loader не поддерживает мемоизацию компонентов с помощью React.memo, так что при написании декораторов для MobX надо будет учесть это для удобства локальной разработки. Еще одно вызванное этим неудобство — при включенной настройке Highlight Updates в React Developer Tools при любом взаимодействии с приложением обновляются все компоненты. Поэтому при локальной работе над оптимизацией производительности следует отключать настройку HOT_RELOAD. Оптимизация сборки в Webpack 4 выполняется автоматически при указании mode: 'development' | 'production'. В данном случае положусь на стандартную оптимизацию (+ включение параметра keep_fnames: true в terser-webpack-plugin для сохранения названия компонентов), так как она уже качественно настроена. Отдельного внимания заслуживает разбиение на чанки и контроль клиентского кэширования. Для корректной работы нужно: Для ускорения сборки в этом проекте использую thread-loader — при параллелизации на 4 процесса он дал ускорение сборки на 90%, что лучше, чем у happypack при аналогичных настройках. Настройки для лоадеров, в том числе для babel, в отдельные файлы (вроде .babelrc) выносить, полагаю, излишне. А вот конфигурацию кроссбраузерности удобнее держать в параметре browserslist основного package.json, так как он используется также для autoprefixer'а стилей. Для удобства работы с Prettier сделал параметр AGGREGATION_TIMEOUT, который позволяет установить задержку между обнаружением изменений в файлах и пересборкой приложения в режиме dev-server. Так как я настроил переформатирование файлов при сохранении в IDE, то это вызывает 2 пересборки — первую на сохранение исходного файла, вторую на завершение форматирования. 2000 миллисекунд обычно достаточно, чтобы webpack дождался финальной версии файла. Остальная конфигурация особого внимания не заслуживает, так как раскрыта в сотнях обучающих материалов для начинающих, поэтому можно переходить к проектированию архитектуры приложения. Раньше для создания тем приходилось делать несколько версий *.css файлов и перезагружать страницу при смене темы, загружая нужный набор стилей. Сейчас все легко решается с помощью Custom CSS Properties. Данную технологию поддерживают все целевые браузеры текущего приложения, но есть и полифиллы для IE. Допустим, будет 2 темы — light и dark, наборы цветов для которых будут находиться в Для того, чтобы эти переменные применялись глобально, их нужно записать в document.documentElement, соответственно нужен небольшой парсер, чтобы преобразовать этот файл в javascript объект. Позже расскажу, почему так удобнее, чем сразу хранить в javascript. Здесь же проверяется согласованность тем — то есть полное соответствие набора переменных, при различии которых сборка падает. При использовании этого лоадера получается вполне красивый объект с параметрами, и достаточно пары строк для утилиты смены темы: Предпочитаю перевести эти css-переменные в стандартные для *.scss: IDE WebStorm, как видно на скриншоте, показывает цвета на панели слева и по клику на цвет открывает палитру, где можно его сменить. Новый цвет автоматически подставится в themes.scss, сработает Hot Reload и приложение моментально преобразится. Это именно тот уровень удобства разработки, который и ожидается в 2019 году. В данном проекте буду придерживаться дублирования названий папок компонентов, файлов и стилей, например:  Соответственно, package.json будет иметь содержание { "main": "Chart.js" }. Для компонентов с множественными именованными экспортами (например, утилит) название главного файла будет начинаться с подчеркивания: А остальные файлы будут экспортироваться в виде: Это позволит избавиться от дублирования названий файлов, чтобы не теряться в десятке открытых index.js / style.scss. Можно решить это и плагинами к IDE, но почему бы и не универсальным способом. Компоненты буду группировать постранично, кроме общих вроде Message / Link, а также по возможности использовать именованные экспорты (без export default) для поддержания однообразия названий, простоты рефакторинга и поиска по проекту. Файл, который служит entry point для Webpack, будет выглядеть следующим образом: Так как при работе с observables в консоли выводится что-то вроде Proxy {0: "btc", 1: "eth", 2: "usd", 3: "test", Symbol(mobx administration): ObservableArrayAdministration}, в полифиллах сделаю утилиту для приведения в стандартный вид: Также в основном файле подключаются глобальные стили и нормализация стилей для разных браузеров, при наличии ключа для Sentry в .env.frontend начинают логироваться ошибки, создается MobX хранилище, инициируется слежение за изменениями параметров с помощью autorun и обернутый в react-hot-loader компонент монтируется в DOM. Само хранилище будет представлять из себя не-observable класс, параметрами которого будут не-observable классы с observable параметрами. Таким образом подразумевается, что набор параметров не будет динамическим — следовательно, приложение будет более предсказуемым. Это одно из немногих мест, где пригодится JSDoc, чтобы включить автодополнение в IDE. Пример MobX стора можно разобрать на примере GlobalStore, у которого будет на данный момент единственное назначение — хранить и устанавливать текущую стилевую тему. Иногда параметрам и методом класса вручную с помощью декораторов устанавливают тип, например: Но смысла в этом не вижу, так как старый Proposal декораторов класса поддерживает их автоматическую трансформацию, поэтому достаточно следующей утилиты: Для использования необходимо откорректировать плагины в loaderBabel.js: , а в настройках ESLint соответственно выставить parserOptions.ecmaFeatures.legacyDecorators: true. Без этих настроек в target декоратора передается только дескриптор класса без прототипа, и, несмотря на тщательное исследование текущей версии Proposal, я не нашел способа обернуть методы и статические свойства. В целом настройка хранилища закончена, но хорошо бы еще раскрыть потенциал MobX autorun. Для этого как нельзя лучше подойдут задачи типа «дождаться ответа от сервера авторизации» или «загрузить переводы с сервера», после чего записать ответы в стор и непосредственно отрендерить приложение в DOM. Поэтому забегу немного в будущее и создам стор с локализацией: Как видно, есть некие файлы *.json с переводами, а в конструкторе класса эмулируется асинхронная загрузка с помощью setTimeout. При его выполнении в недавно созданном GlobalStore проставляется маркер this.rootStore.global.shouldAppRender = true. Таким образом, из app.js нужно перенести функцию рендеринга в файл autorun.js: В функции initAutorun может быть сколько угодно autorun конструкций с коллбэками, которые сработают только при собственной инициации и изменении переменной внутри конкретного коллбэка. В данном случае в консоль будет выведено autorun-shouldAppRender GlobalStore@3.shouldAppRender changed to true;, и вызван рендеринг приложения в DOM. Мощный инструмент, позволяющий логировать все изменения в сторе и соответственно на них реагировать. Перевод на другие языки — одна из самых объемных задач, в небольших компаниях зачастую недооцененная в десятки раз, а в крупных — излишне переусложненная. От ее реализации зависит, сколько нервов и времени не будет потрачено впустую сразу у нескольких отделов в компании. Затрону в статье только клиентскую часть с заделом на будущую интеграцию с другими системами. Для удобства разработки фронтенда необходимо иметь возможность: Под эти условия подходит, к примеру, следующая схема: в каждом компоненте с текстами будет лежать файл messages.js с базовыми значениями для разработки (которые в идеале никогда не увидит клиент) в виде обычного объекта с параметрами. Вставка в компонент будет происходить однострочно с помощью хука. Полное название параметра будет формироваться автоматически по пути к файлу в проекте (при необходимости можно легко обфусцировать / сократить), что исключит пересечение названий. Функции преобразования текста (вставка переменных, склонений, чисел) выполняются последовательно. Должно получиться удобно. Так как уже есть стор с локализацией, в котором лежит currentLanguage и объект i18n с потенциально присутствующими переводами, можно написать хук, который будет получать оттуда тексты. Сам функциональный компонент имеет имя по названию файла, а на экспорт идет подключенный к MobX-стору автоматически обновляемый компонент с суффиксом, к примеру, Connected. Возможно, стоит внести подобное правило именования в ESLint, чтобы явно отличать подключенные к стору компоненты. Декоратор observer представляет собой обертку над mobx-react-lite/useObserver, которая при выключенном HOT_RELOAD оптимизирует обновление компонентов с помощью React.memo (в прошлом PureMixin / PureComponent), а при включенном просто оборачивает в useObserver все содержимое компонента: Внимания заслуживает только передача displayName на каждом этапе, чтобы в React-инспекторе были красивые названия элементов (на stack trace ошибок не влияет). Теперь нужен хук для вставки RootStore: Который можно легко использовать в любом компоненте, обернутом в observer: Возвращаясь к созданному выше компоненту TestLocalization — осталось лишь сделать хук useLocalization: Функции replaceDynamicParams и replacePlurals написаны для конкретного примера — вместо них можно использовать любой шаблонизатор для конкретных языков проекта и поддерживающий, например, строки с включенными объектами, массивы, форматирование дат, склонение имен и городов и т.п. Данный хук принимает в себя системную константу от Webpack — __filename — и объект с сообщениями, а возвращает функцию, которая непосредственно сходит в стор за значением. При желании можно включить отображение сообщений об отсутствии переводов, хотя при разработке это не нужно — переводы будут приходить на стенды из системы локализации, соответственно локально их все равно не будет, а отобразится значение по умолчанию. Но если все же включить, то сейчас в консоли отобразится: Если же добавить локализацию для данного поля в ru.json: То все заработает, как и ожидалось. А при добавлении в файл src/localization/en.json аналогичного перевода заработает и смена языков «на лету» с помощью метода setLocalization из I18nStore. Можно сделать и «привычный» в экосистеме React компонент Message: Так как нужно каждый раз передавать переменную __filename (либо каждый раз уникальный id как в страшном сне разработчика), то импорт этого компонента будет немного необычным, однако использование стандартным: Из особенностей — при использовании в компоненте хука useLocalization и смене языка обновится весь компонент (так как он подписывается на изменение currentLanguage, а при использовании компонента Message — только сам текст. Однако это редкая операция, да и затраты на перерендеринг приложения при смене языка копеечные, поэтому я бы пользовался напрямую хуком. В завершение темы можно подумать, как удобнее в будущем состыковать этот подход с системой локализации (под ней подразумеваю административное приложение, в котором переводчики узнают о недостатках переводов, делают свои предложения в виде черновиков, менеджер / тестировщик проводят проверку на стенде и прикрепляют определенные черновики к релизам приложения в production). Так как в текущей схеме уникальные id параметров привязаны к пути к файлу, то можно при деплое на стенд пробегаться по всем messages.js и формировать *.json файл со списком всех переменных, привязанный к выкладываемой ветке. Затем этот файл автоматически загружать в систему локализации и дожидаться от переводчиков подходящих переводов (а в системе им подсветятся недостающие / удаленные), после чего осуществлять выкладку в production. Семантичность названий параметров и указание на файлы, в которых были правки, очень поможет переводчикам. В целом в связке MobX + Hooks клиентская локализация выглядит удобно. Для перевода констант и сообщений, приходящих с backend, нужно будет написать функцию, работающую непосредственно в сторе, с однотипным механизмом. Ключевой момент при работе с любыми сторонними данными (с backend, из открытых источников или от пользователя) — это надежная валидация, которая даст уверенность, что фронтенд будет работать предсказуемо. Также полезно иметь централизованный список всех возможных запросов, с описанными передаваемыми и приходящими параметрами. Я бы реализовал это так: К примеру, есть стор, содержащий информацию об открытой торговой паре. Для получения данных вызывается метод fetchSymbol, в который передается id необходимой валюты и валюта, к которой идет торговля. Далее выполняется запрос через утилиту, при успехе — в единой транзакции обновляются данные в сторе (так как все методы автоматически оборачиваются в @action.bound), а при ошибке она логируется в Sentry благодаря декоратору в функции инициализации: Данный запрос наиболее показателен, так как использует сразу весь функционал валидации запросов: Схема работы функции request следующая: Подобная схема позволяет быть полностью уверенным в получаемых данных и оперативно реагировать на залогированные ошибки. Например, так выглядит сообщение в Sentry, если не совпал тип одной из переменных в response:  Впоследствии необходимо будет внедрить глубокую валидацию ответа — на отзывчивость приложения выполнение простой функции несколько тысяч раз влияет слабо (при необходимости валидацию можно выполнять в отдельном потоке), но многократно повышает стабильность. Тема еще более объемная, и заслуживает отдельной статьи соразмерной с данной, но проговорить принципы и сделать минимальную реализацию можно. Так, для хорошего роутера нужны: Для начала сделаю только «скелет», который позволит разблокировать развитие продукта и беспрепятственно начать писать компоненты и бизнес-логику, так как полнофункциональный роутинг — задача не на одну неделю. Для начала понадобятся файлы с конфигурацией роутов: Компоненты, соответствующие роутам, вынесены в отдельный файл для защиты от цикличной зависимости — если в компонентах использовать удобные конструкторы ссылок вида <Link route={routes.marketDetailed}>, то возникнет цикличный импорт. Webpack в некоторых случаях умеет справляться с этим, но лучше не полагаться на удачу. Теперь необходим стор, который двусторонне свяжет location и подходящий роут из списка выше. Схема работает достаточно просто — в конструкторе стора осуществляется поиск подходящего роута из routes.js и проверка всех динамических параметров по маске. Если роут не найдет или параметр не соответствует маске — происходит редирект на страницу с ошибкой 404. Разумеется, при развитии роутера нужно включить возможность «найти максимально похожий роут и перейти на него с дефолтными параметрами», и эту же стратегию использовать, если данные все же прошли маску, но некорректные — например, пользователь попробовал запросить данные по торговой паре 'test-test'. Далее в currentRoute записывается подходящий роут, обогащенный объектом params (значения переменных из URL) и isLoading: true. Теперь свою работу может начать React-компонент Router: Когда рендерится этот компонент, хранилище уже давно инициализировалось и нашло подходящий роут, поэтому смысла проверять на currentRoute == null нет. Основная идея компонента — если у текущего роута параметр isLoading === true, то передавать этот параметр в компонент и менять его на false только после того, как полностью выполнится route.beforeEnter (если есть). Вместо описанной в принципах роутинга необходимости применять стратегии отказоустойчивости здесь постыдный console.error, как напоминание о том, что работы еще предстоит море. Следующий вопрос, который предстоит решить — где и когда запрашивать данные, необходимые для страницы. В React-сообществе распространены 2 подхода: Можно удобно реализовать любой из этих подходов, но для торгового интерфейса все же выберу второй — раньше у бирж встречал модульность (когда работает все, кроме графика, или работает только график без балансов и списка торговых пар и т.п.), но со временем они тоже отказались от этого подхода. Поэтому все запросы буду делать в методе beforeEnter, по схеме: «оптимистичное обновление», последовательные запросы за данными (в будущем, конечно, необходимо использовать параллельные с возможностью прерывания), при различных ошибках — различные стратегии исправления (блокирование всей страницы с перезапросом всех данных несколько раз — если сервер отвечает 500 ошибками; откат до предыдущего состояния приложения если сервер отвечает некорректными данными; нотификация о том, что определенный блок с данными сейчас недоступен; редирект на роут с дефолтными параметрами и т.п.). В данном «скелете» приложения это все не реализовано, так как для самого продукта на этапе MVP пользы принесет мало. Последним элементом роутинга будет компонент для создания ссылок: Этот компонент принимает параметр route, валидирует переданные динамические params (если есть) на этапе создания ссылки (чтобы фронтенд сам себя не смог поломать при клике) и заполняет href заполненным адресом. Кроме этого, если у текущего роута все еще загружаются данные в методе beforeEnter, переход по ссылке блокируется. Можно показывать нотификацию из разряда «подождите, идет загрузка», либо откладывать переход до завершения текущей загрузки, либо прерывать все запросы и форсированно переходить на новую страницу — в зависимости от потребности. Касательно бизнес-метрик (переходы на страницы, клики по кнопкам, отправка заполненных форм, количество возникновения ошибок, поведение пользователя на странице) в общем случае достаточно Яндекс.Вебвизор или аналога с автоматическим сбором. Для приложения биржи не нужна система замера сложнодостижимых целей вроде заполнения многостраничных форм и выполнения комплекса действий. А вот отзывчивость приложения — время полной отрисовки страницы и длительность запросов за данными, включая валидацию — измерять необходимо, чтобы иметь возможность сделать эффективную точечную оптимизацию. Измерить длительность запросов можно было бы такой утилитой: И включив эти две middleware в функцию request: Таким образом, длительность запросов аггрегируется, если перерыв между ними составил более 2 секунд, и отправится в некую систему мониторинга (в данном случае в консоль) единым массивом. Часто замеры производительности производится только на сервере — от поступления запроса до отдачи результата, но эта практика дает однобокую статистику — в реальности у клиента запрос может выполняться в разы дольше, и нужно собирать детализированную (в идеале) статистику по каждому этапу запроса, исходя из клиентского опыта. Теперь можно приступать к кодированию бизнес-логики и компонентов — в этом демо полностью реализовал первоначальное задание заказчика. Про принципы и значение end-to-end тестирования информации много, в том числе в документации к выбранному инструменту Cypress. С точки зрения разработки в них важны: легкость развертывания; удобство поддержки и развития, в том числе для тестировщиков; легкость встраивания в Continious Integration. Так как данный инструмент состоит всего из одного пакета и написан на javascript с похожим на Chai / Sinon синтаксисом, обычно проблем с развитием инструментария не возникает. Однако не радует долгая установка самого пакета, поэтому устанавливать желательно в отдельную папку, в моем случае — ./tests, в package.json которого будет единственная зависимость — "dependencies": { "cypress": "3.2.0" } После установки инструмент сам создаст структуру папок с многочисленными примерами для обучения. Для максимального удобства нужно синхронизировать возможности встроенного в него Webpack и общей конфигурации проекта: Для этого потребовалась установка всего лишь одного пакета в основной проект. Синхронизировать достаточно лишь module (для использования идентичного синтаксиса) и resolve (чтобы работали все алиасы и импорты файлов как в основном проекте). В плагинах ESLint для корректного распознавания глобальных переменных (вроде describe, cy) нужен дополнительный плагин eslint-plugin-cypress. На этом настройка закончена, и вот так может выглядеть проверочный тест: Так как на текущий момент Cypress не поддерживает протокол fetch, можно применить полифилл, и заодно указать роуты для запросов: Собственно все, можно создавать удобные команды и обучать тестировщиков. Да, на этом шаге создана неблокирующая бизнес-разработку архитектура. Можно написать небольшую документацию по использованию и заложенным принципам, составить стандарты кодирования и приема качества, и привлечь коллег-разработчиков для кодирования компонентов / бизнес-логики / стилей. А в это время, честно отдавая себе отчет, что по описанным принципам работа сделана едва ли на половину, месяц-два добрабатывать, тщательно покрывая юнит-тестами утилиты перед тем, как переходить к следующим этапам архитектуры (real-time взаимодействие, подключение serviceWorker, интеграция в CI, кроссбраузерность и полифиллы, гибкое управление правами отображения и функционирования элементов, настройка бизнес-метрик, мобильная версия, автоматизация рутинных задач и т.п.). Размеры итоговых файлов (с Gzip) вполне адекватны:  И структура компонентов в React Developer Tools выглядит очень приятно:  В целом работать в связке React Hooks + MobX понравилось намного больше, чем с Redux. Надеюсь, мой взгляд на архитектуру приложения поможет избежать лишних затрат времени. Если же выпустить продукт хотя бы без одного из описанных элементов, велика вероятность, что он понадобится в то время, когда внедрение займет месяцы и будет сопряжено со сложным рефакторингом. Лучше спроектировать это на этапе создания архитектуры. Всем интересной разработки! Весь код Благодарю сообщество за конструктивные комментарии и письма, после использования описанной выше архитектуры назрел довольно большой список доработок, которыми рад поделиться: 1. Отсутствие yarn.lock приводило к разным хешам после сборки, как и использование yarn install --force, но проблема легко решилась скриптом "upd": "yarn install && yarn add file:./eslint-custom && yarn add file:./webpack-custom". В данном случае обновляются зависимости только у не влияющих на итоговую сборку ESLint и Webpack. 2. Пути к папкам в webpack-custom/config/configOptimization.js, описаны не кроссплатформенно, и вместо должно быть 3. При работе с локализацией передавать каждый раз параметры в useLocalization(__filename, messages) утомительно, выход — переделать параметры в объекты вида например, с помощью функции Но так как в IDE в таком случае перестает работать автодополнение, это преобразование можно выполнять в лоадере Webpack: Соответственно, файлы messages.js теперь имеют содержание: Также для повышения удобства работы с текстами очень пригодилась утилита, вызываемая в app.js, которая пробегается по всем файлам messages.js и показывает, каких ключей не хватает в *.json файлах локализации: Таким образом, в консоли всплывет сообщение вида которое нужно просто скопировать в соответствующий *.json файл — разумеется, только если в проекте нет отдельной системы для работы с переводами, а тексты лежат в репозитории. 3. Валидаторы для АПИ в виде функций Lodash вроде someResponseParam: _.isString хотя и работают, но не расширяемы и не дают возможность вывести текст ошибки в удобном виде. Поэтому лучше обернуть их в кастомный объект: Такая небольшая обертка позволяет включать название валидатора в текст ошибки — например, с сервера пришел некорректный параметр someResponseParam [isString], сразу давая понять, какой тип был ожидаем. Также можно указать someResponseParam: validators.isString.notRequired, что сделает параметр необязательным. В реальном проекте в дополнение к этим валидаторам понадобились динамические, которые выбирают тип ответа в зависимости от отправленных параметров, а также проверки массивов вида someResponseArray: arrayShape({ someParam: isString }), которые без проблем интегрируются в подобную схему. 5. Роутинг, как и ожидалось, потребовалось значительно развить. Основное изменение — поддержка анимированных переходов между страницами (добавление к body классов isEntering, isLeaving с соответствующей задержкой) и запрет перехода на другие страницы в виде функции beforeLeave (аналог Prompt из react-router), в которой можно выводить предупреждения и блокировать переход при возврате false-значения в промисе, например В целом получилось крайне удобно, когда роуты сами ходят за данными и могут многократно редиректить на другие без рендеринга каких-либо компонентов. Например, на одной из страниц использовалась следующая цепочка:  — при заходе на /some/long/auth/path ее beforeEnter редиректит на алиас /auth
 — beforeEnter роута /auth проверяет авторизацию, и если человек авторизован — то редиректит на /profile
 — beforeEnter роута /profile делает запрос за данными, и если видит, что у пользователя не заполнено имя, редиректит на /profile/edit Эти редиректы проходят быстро и незаметно, при необходимости можно не сохранять их в истории браузера — то есть вместо window.history.pushState использовать location.replace. При этом зоны ответственности четко разграничены, и любое комбинирование редиректов линейно приведет к единственно верному адресу страницы. В этом подход «ходить за данными в роутере» на голову выше подхода «ходить за данными в componentDidMount компонента», если иметь в виду критически важные для отображения запросы, а не загрузку словаря для селекта при его первичном рендере. 6. Классическая задача по отображению лоадера в кнопках при отправке запроса (например, при сабмите форм) элегантно решилась одним декоратором: Такой подход освобождает от создания и обновления дополнительных параметров, по сути лишних. Второй репозиторий с обновленным кодом создавать не буду, так как изменений много, и итоговый код уже не будет соответствовать статье. Лучше потрачу это время на новую статью — интерес к приведенному стеку оказался высок, и есть множество сложных тем, в которых, возможно, мой опыт будет кому-то полезен. Front-end архитектор