# React-Query — Общий обзор и мотивация к применению
Статья о том, как фронтенд-команде компании Чиббис, выдалась возможность построить с нуля новый проект и использовать в нем новые (для нас в компании) подходы и инструменты, в частности 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 Loading...;
}
if (error) {
return Error: {error.message};
}
return (
{data.order}
);
};
Использовав хук 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.