Привет, я Григорий Зароченцев, ведущий фронтенд-разработчик Тинькофф в команде интернет-эквайринга. Сегодня хочу рассказать, что такое компонентный стор, как изолированные хранилища помогают сэкономить кучу кода при разработке и почему глобальный стор — это одновременно и хорошо и плохо.
Поговорим о том, как наша команда пришла к такому подходу, какие плюсы принесло это решение и почему, если вы пишете на Angular, вам стоит хотя бы взглянуть на @ngrx/component-store.
Введение
Наш проект — большой монорепозиторий на Angular и NX, состоящий из нескольких независимых приложений и кучи общего кода. Нам приходится иметь дело с большим количеством разных данных: как-то их кэшировать, обрабатывать и передавать между разными слоями приложения. Без хорошего масштабируемого хранилища тут не обойтись.
Мы используем NGRX. Это хороший и довольно гибкий инструмент, о котором слышал каждый или почти каждый. Наши потребности он закрывает почти на 100%.
Зачем же тогда нужна очередная статья, описывающая NGRX?
Случился кейс, который попал в тот небольшой процент случаев, когда существующих возможностей NGRX нам не хватило. Задача казалась простой: внедрить в одном месте изолированное хранилище, которое не зависело бы от глобального стора, но могло бы с ним взаимодействовать и соблюдать общий флоу. Решение нашлось довольно быстро: библиотека @ngrx/component-store от создателей NGRX, которая позволяет создавать изолированные хранилища на уровне компонента. О ней я и расскажу.
Но сначала пару слов о том, почему долгое время все было нормально, о том самом кейсе и о том, почему вам вообще может понадобиться изолированное хранилище.
Самый большой плюс глобального стора — это…
Его глобальность.
Иметь единственный центральный источник данных очень удобно. Мы всегда можем отследить, откуда данные пришли, как они обрабатываются, сохраняются и передаются куда-то дальше. В NGRX состояние меняется простыми функциями, селекторы всегда отдают актуальные данные, а в компонентах не нужно беспокоиться практически ни о чем, кроме правильного вызова нужных экшенов.
В NGRX все взаимодействия осуществляются асинхронно через потоки RxJS. Логику по инициализации и менеджменту подписок он берет на себя, нам остается лишь отписаться от всего в нужный момент.
Такой код легко читается и легко масштабируется, позволяя разносить его на различные lazy-модули, что идеально ложится на общий флоу Angular. Проект становится легко поддерживать в будущем по мере его роста. А также покрывать юнит-тестами, ведь каждая его часть логически независима.
Казалось бы, все идеально, все проблемы решены. Но у такого подхода есть один большой минус.
Самый большой минус глобального стора — это…
Его глобальность.
По мере роста приложения структура хранилища тоже будет увеличиваться. NGRX позволяет масштабировать его чуть ли не до бесконечности, но с этим растет и наша ответственность как разработчиков: нет ли пересекающихся экшенов, не приведет ли изменение данных в одном месте к побочным эффектам в другом и так далее.
Но если мы на 100% уверены, что код работает идеально и не содержит никаких ошибок, не забыли ли мы правильно очищать стор при уничтожении компонентов? Его части не очищаются автоматически, во всяком случае в NGRX. Поэтому необходимость следить за тем, чтобы состояние было корректным при уничтожении компонента или его повторной инициализации, ложится на наши плечи, иначе пользователь увидит страницу с некорректными данными.
К тому же глобальным стором очень неудобно решать задачу, когда какой-то компонент необходимо разместить на странице несколько раз или добавлять/удалять динамически. Вы можете сказать: «А зачем такую задачу решать с использованием NGRX? Описываете инпуты, аутпуты — и все, а само состояние храните уже в сторе». И будете правы, но лишь отчасти. Представьте, что у вас есть абстрактный компонент: он прост, не перегружен сложной бизнес-логикой, но главное — он может взаимодействовать со своим изолированным API. И мы хотим переиспользовать его в любом месте проекта, не импортируя дополнительно в текущем модуле ничего, кроме него самого. А еще он может повторяться на странице несколько раз, и управлять им вы хотите средствами NGRX. Представили?
Тот самый кейс
С такой ситуацией мне пришлось столкнуться. Стояла задача сделать загрузчик файлов — что может быть проще? Но это только на первый взгляд. Требования у него не совсем обычные:
пользователь может выбрать тип загружаемых файлов или сразу несколько типов;
каждый тип будет иметь свой UI. Например, для паспорта необходимо загружать два разворота, для дипломов — еще и приложения, а остальные файлы имеют UI по умолчанию;
пользователь должен видеть прогресс загрузки и предпросмотр файлов;
API для загрузки всегда один и тот же.
И этот компонент мы хотим сделать общим на весь проект. На странице он может отображаться как в шаблоне, так и в диалоговом окне, а еще таких загрузчиков может быть несколько штук на странице. Компонент должен взаимодействовать с глобальным стором, и всю логику внутри него мы хотим организовать через потоки RxJS. Да и в целом флоу NGRX поможет соблюсти единство общей архитектуры.
И тут мы задаемся вопросом: и как же это реализовать? Где и как в приложении хранить данные о файлах, их типах, состоянии загрузки и так далее? Пока компонент существует в единственном экземпляре, все можно хранить в глобальном сторе и никаких проблем нет: закрыли страницу — очистили стор. Но что делать, если компонентов на странице может быть два, три или десять? В голову приходят несколько вариантов:
использовать конструкции с ключами по типу loader_1/loader_2/loader_3;
использовать в сторе вместо простых объектов массивы объектов;
отказаться от NGRX.
Все варианты из предложенных выше, если честно, так себе. Использовать ключи неудобно, как и массивы. В них легко запутаться: сегодня мы помним, какой индекс с чем соотносится, а завтра, скорее всего, уже нет. К тому же использование динамических объектов, которые невозможно строго типизировать, считается плохим паттерном в мире TypeScript. А от NGRX отказываться — ну тут без комментариев