Статья о том, как фронтенд-команде компании Чиббис, выдалась возможность построить с нуля новый проект и использовать в нем новые(для нас в компании) подходы и инструменты, в частности React-Query(про FSD и Tramvai в следующих статьях). Какие преимущества нам дал RQ, нашлись ли недостатки, целесообразность использования его в новых и существующих проектах.
Общая информация, предыстория, терминология
Этим летом команда Чиббиса успешно выпустила новый проект — обновленный личный кабинет партнера (ЛКП). Это приложение, которым пользуются наши партнеры — рестораны для работы с сервисом. Там они могут работать со своими заведениями (открывать/закрывать, обрабатывать заказы и работать с меню). Нашей основной целью было отказаться от старого ЛК, верой и правдой прослужившего компании 9 лет, но неизбежно превратившегося в сложно поддерживаемое легаси. При этом в разработке нового приложения мы ставили себе также и исследовательские задачи: попробовать новый стек и новые подходы к разработке. Мы хотели, чтобы при удачном раскладе новый ЛКП стал образцово показательным проектом, по образу и подобию которого мы бы подались в рефакторинг существующих и создание новых продуктов. В результате предварительно проделанной исследовательской работы в качестве основного фреймворка мы выбрали Tramvai, а архитектуру приложения решили строить по FSD-подходу. Помимо этого мы решили пересмотреть работу со стейт-менеджментом в приложении, о чем подробнее и расскажу.
Исторически сложилось, что на web-проектах в Чиббисе для стейт-менеджмента используется Redux. Redux в целом справляется с возложенными на него обязанностью хранить стор приложения, однако тянет за собой большое количество бойлер-плейта. Разработчики, работавшие с существенным количеством редьюсеров поймут, о чем речь: файлы констант, экшенов, селекторов, структурных селекторов, редюсеров, типов и т.д. могут занимать сотни файлов и тысячи строк.
Поскольку при разработке нового проекта целью было найти способы оптимизации кодовой базы, упростить работу и повысить производительность разработчиков, появилась идея попробовать работать не с общим, а с разделенным на серверную и клиентскую части стором. Так мы перешли на React-Query.
React-Query (далее RQ) — JavaScript-библиотека, упрощающая работу с получением и кэшированием данных в React-приложении. Разработана компанией TanStack и активно развивается: в 2020 году была выпущена версия 1.0, а текущая версия — 5.0.
Клиентский стор (локальный) отвечает за хранение состояния ui-компонетов и шаринг этого состояния между ними: показ/скрытие, счетчики (например, таймер на форме), выбор пользователя и т.д. Например, модальные окна часто нуждаются в глобальном сторе, чтобы их можно было скрывать и показывать из любого компонента.
Серверный стор нужен для хранения на серверной стороне данных, необходимых для использования в веб-приложении: профиль пользователя, список товаров и т.д. Особенностью работы с серверным стором является необходимость учитывать состояние запросов по конкретным данным (успех, в процессе, ошибка и т.п.), на хранение которых отводятся (обычно) отдельные ключи в сторе.
Идея об отдельном сторе для серверных данных появилась неслучайно: по мере роста приложений их общие сторы сильно разрастаются, в основном за счет данных, полученных по API с бэкенда.
На рисунке ниже представлена часть стора реального приложения. Видно, что серверных данных гораздо больше, чем клиентских. Редьюсер, хранящий ответы API, содержит также ключ “ui“, в котором хранится состояние запроса.
Пример общего стора
Посмотрим, как реализована работа с запросами в приложении. Рассмотрим пример получения неоплаченных заказов:
// ...imports export const unpaidInfoEpic: TFetchUnpaidInfoEpic = (action$) => action$.pipe( ofType(CHECKOUT_UNPAID_INFO_FETCH), debounceTime(REQUEST_MS), mergeMap(({ payload }) => concat( of(checkoutUnpaidInfoFetchPending()), combineLatest([timer(MIN_LOADING_MS), fetchUnpaidInfo(payload)]).pipe( map((x) => x[1]), switchMap((response) => { const { status, response: data } = response; if ([200].includes(status)) { const result = [ checkoutUnpaidInfoFetchSuccess({ status, }), checkoutUnpaidInfoSave(data), ]; return result; } return [ checkoutUnpaidInfoFetchError({ status }), ]; }), catchError((error) => { const { status } = error; console.log(error); return [ checkoutUnpaidInfoFetchError({ status }), ]; }), ), )), );
Этот код выполняет свои обязанности: он посылает запрос к API, получает данные и сохраняет их в нужное место, параллельно записывая состояние запроса.
Код не существует в вакууме, для его работы нужна “обвязка“ из actions (делаем изменения в редьюсере), constants, reducer (реагирует на actions), selectors (для удобного получения данных компонентами и их (данных) мемоизации). Так же не стоит забывать, что для всего нужны еще и типы.
Взяв один конкретный пример работы с запросом — unpaidInfoEpic, я посчитал количество обслуживающего только его кода (без учета функции обращения к апи fetchUnpaidInfo ): получилось…
…более 280 строк! Многовато для простого похода за данными на сервер. Хотя объем кода — это не проблема и нет цели именно в сокращении его объемов. Однако разработка идет вперед, в современных реалиях приложение должно легко модифицироваться и масштабироваться. Хорошо бы как то упростить процесс взаимодействия с API.
Преимущества React-Query
Итак, вернемся к RQ. В чем же состоят его сильные стороны?
-
Забирает на себя работу по хранению серверного состояния, синхронизацию хранимых данных и упрощает работу с запросами к API
-
Простота использования
-
Кэширование данных
-
Автоматическая инвалидация
-
Отслеживание статуса запроса в режиме реального времени
-
Возможности более тонкой настройки
-
Интеграция с React
Рассмотрим эти преимущества поподробнее.
Одним из главных плюсов RQ является простота его использования. Библиотека предоставляет интуитивно понятный API и простые концепции, которые делают работу с асинхронными запросами и управлением состоянием очень удобной. Рассмотрим основные аспекты, которые делают RQ простым в использовании:
-
Хуки для работы с данными: useQuery, useMutation, usePaginatedQuery и другие хуки упрощают выполнение запросов и получение данных в компонентах React.
-
Декларативный подход: при использовании RQ нет необходимости писать много кода для управления состоянием и выполнения запросов. Библиотека позволяет декларативно описывать данные, которые вам нужны, и предоставляет простые способы их получения и обновления, освобождая от рутинных задач.
-
Встроенное кэширование: RQ автоматически кэширует полученные данные и обновляет кэш при необходимости. Это позволяет избежать повторных запросов к серверу и снижает нагрузку на сеть и сервер. Кроме того, кэшированные данные можно легко инвалидировать и обновлять при изменении данных на сервере, а так же отключить для тех запросов, где они не нужны.
-
Умное управление состоянием: библиотека предоставляет удобные способы управления состоянием при выполнении асинхронных операций. RQ позволяет легко отслеживать статус запроса (загрузка, успех, ошибка), обрабатывать и отображать соответствующие состояния в пользовательском интерфейсе.
-
Расширяемость: RQ предоставляет API для настройки и расширения его функциональности в соответствии с вашими потребностями. Вы можете настроить параметры запросов, управлять временем жизни кэша, расширять логику обновления данных и добавлять дополнительные функции.
Все это делает RQ очень простым в использовании и интуитивно понятным инструментом для разработчиков. Библиотека позволяет сосредоточиться на создании функционала, не тратя много времени на рутинные задачи по работе с данными и управлению состоянием.
Немного практики
Вернёмся к примеру с неоплаченными заказом. Попробуем реализовать его, используя RQ.
Запрос к API fetchUnpaidInfo уже реализован, менять его нет необходимости. Нет нужды и в каких-то редьюсерах, коннекторах к стору и экшенах — будем получать данные непосредственно в компоненте:
const UnpaidOrder = ({ orderId }) => { const { data, isLoading, error } = useQuery(['unpaidOrder', orderId], () => fetchUnpaidInfo(orderId); ); if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } return ( <div>{data.order}</div> ); };
Использовав хук useQuery, мы получили то, что хотели, написав только саму функцию обращения к API. Весь код обработки запроса свелся к 3м строчкам (2 — 4 строки). RQ забрал на себя: получение данных, отслеживание статуса, а также хранение данных.
-
Вы спросите: «А если полученные данные понадобятся в другом месте приложения? Как мы их расшарим между компонентами?»
-
Очень просто. Библиотека хранит (кэширует) по ключу ‘unpaidOrder’ нужные нам данные — как сам неоплаченный заказ, так и состояние запроса, поэтому в любом другом компоненте мы вызываем следующий query:
const { data } = useQuery(['unpaidOrder'], () => fetchUnpaidInfo(orderId); );
и получаем данные заказа или состояние запроса.
При этом не важно, существует ли еще инстанс нашего самого первого запроса, RQ автоматически предоставит нам всё, что нужно в любом месте приложения, при любом количестве вызовов useQuery для ключа ‘unpaidOrder’.
Механизм работы
При первом вызове useQuery происходит следующее:
-
Отправляется запрос к серверу (fetchUnpaidInfo)
-
состояние query — isLoading (isFetching). Можем отрендерить лоадер.
-
Получен ответ от сервера (состояние query isError || isSuccess || ….). Можем рендерить данные.
-
Полученный результат кэшируется по ключу [‘unpaidOrder’] (по умолчанию cacheTime = 5 минут)
-
результат сразу маркируется как stale (по умолчанию staleTime = 0)
При втором вызове useQuery в другом компоненте до истечения cacheTime (для того же ключа):
-
Данные лежат в кэше, поэтому мгновенно доступны. Можем запускать рэндер.
-
Так как параметр stale равен 0, данные считаются устаревшими и RQ считает нужным в фоне сделать запрос для обновления состояния. Имеем isFetching == true, isLoading == false — это важный для UI нюанс.
-
запрос уходит
-
Состояние обновилось, обновился кэш и время жизни записи в кэше вновь становится 5 минут
При третьем вызове после истечения cacheTime:
-
Garbage Collector получил оповещение удалить данные по ключу ‘unpaidOrder’
-
При повторном вызове начинаем всё сначала
Схема работы RQ
Подробнее о механизме кэширования, инвалидации кэша и его настройке расскажу в следующий раз.
Минусы
-
В уже существующий проект мы привносим новую библиотеку со своей философией и правилами работы. Их необходимо держать в голове при проектировании и рефакторинге.
-
Не является полноценной заменой классического стора (Redux, Mobx), так как не предназначен для хранения клиентских данных.
-
Хранилище не персистентное по умолчанию. Важно настраивать время кэширования (данные могут “протухать”)
-
Возможное усложнение онбординга новых сотрудников (+1 технология для изучения)
Выводы
Сейчас, когда запуск личного кабинета партнера состоялся, и начался процесс эксплуатации — я понимаю, что мы не ошиблись, выбрав React-Query. Инструмент избавил нас от менеджа серверного стейта, в разы сократив кодовую базу. Нам не нужно думать о состоянии запроса, о хранении серверных данных, достаточно помнить о времени жизни кэша и, при необходимости, делать инвалидацию. Использование RQ значительно облегчает процесс доставки и хранения данных между сервером и клиентом. Важным плюсом считаю, что интеграция может проходить поэтапно, без возникновения серьёзных проблем.
Как известно, нет предела совершенству, связка Redux — React-Query, покрывает все наши потребности, но, уже есть мысли об отказе от Redux, в пользу чего то попроще (React Context ?). Напишу что получилось — в следующх статьях.
Ссылки
Официальная документация: Overview | TanStack Query React Docs
The consequences of using State over Cache and its impact on data consistency.
ссылка на оригинал статьи https://habr.com/ru/articles/866654/
Добавить комментарий