# React-Query — Общий обзор и мотивация к применению

Статья о том, как фронтенд-команде компании Чиббис, выдалась возможность построить с нуля новый проект и использовать в нем новые (для нас в компании) подходы и инструменты, в частности React-Query (про FSD и Tramvai в следующих статьях). Какие преимущества нам дал RQ, нашлись ли недостатки, целесообразность использования его в новых и существующих проектах.

a2d31b150eccf44d26e8246d45db0e6e.jpeg

Общая информация, предыстория, терминология

Этим летом команда Чиббиса успешно выпустила новый проект — обновленный личный кабинет партнера (ЛКП). Это приложение, которым пользуются наши партнеры — рестораны для работы с сервисом. Там они могут работать со своими заведениями (открывать/закрывать, обрабатывать заказы и работать с меню). Нашей основной целью было отказаться от старого ЛК, верой и правдой прослужившего компании 9 лет, но неизбежно превратившегося в сложно поддерживаемое легаси. При этом в разработке нового приложения мы ставили себе также и исследовательские задачи: попробовать новый стек и новые подходы к разработке. Мы хотели, чтобы при удачном раскладе новый ЛКП стал образцово показательным проектом, по образу и подобию которого мы бы подались в рефакторинг существующих и создание новых продуктов. В результате предварительно проделанной исследовательской работы в качестве основного фреймворка мы выбрали Tramvai, а архитектуру приложения решили строить по FSD-подходу. Помимо этого мы решили пересмотреть работу со стейт-менеджментом в приложении, о чем подробнее и расскажу.

Исторически сложилось, что на web-проектах в Чиббисе для стейт-менеджмента используется Redux. Redux в целом справляется с возложенными на него обязанностью хранить стор приложения, однако тянет за собой большое количество бойлер-плейта. Разработчики, работавшие с существенным количеством редьюсеров поймут, о чем речь: файлы констант, экшенов, селекторов, структурных селекторов, редюсеров, типов и т.д. могут занимать сотни файлов и тысячи строк.

Поскольку при разработке нового проекта целью было найти способы оптимизации кодовой базы, упростить работу и повысить производительность разработчиков, появилась идея попробовать работать не с общим, а с разделенным на серверную и клиентскую части стором. Так мы перешли на React-Query.

React-Query (далее RQ) — JavaScript-библиотека, упрощающая работу с получением и кэшированием данных в React-приложении. Разработана компанией TanStack и активно развивается: в 2020 году была выпущена версия 1.0, а текущая версия — 5.0.

Клиентский стор (локальный) отвечает за хранение состояния ui-компонетов и шаринг этого состояния между ними: показ/скрытие, счетчики (например, таймер на форме), выбор пользователя и т.д. Например, модальные окна часто нуждаются в глобальном сторе, чтобы их можно было скрывать и показывать из любого компонента.

Серверный стор нужен для хранения на серверной стороне данных, необходимых для использования в веб-приложении: профиль пользователя, список товаров и т.д. Особенностью работы с серверным стором является необходимость учитывать состояние запросов по конкретным данным (успех, в процессе, ошибка и т.п.), на хранение которых отводятся (обычно) отдельные ключи в сторе.

Идея об отдельном сторе для серверных данных появилась неслучайно: по мере роста приложений их общие сторы сильно разрастаются, в основном за счет данных, полученных по API с бэкенда.

На рисунке ниже представлена часть стора реального приложения. Видно, что серверных данных гораздо больше, чем клиентских. Редьюсер, хранящий ответы API, содержит также ключ «ui», в котором хранится состояние запроса.

46e1d744b4c27a1770501fa7c9f1b409.jpeg

Пример общего стора

Посмотрим, как реализована работа с запросами в приложении. Рассмотрим пример получения неоплаченных заказов:

// ...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 простым в использовании:

  1. Хуки для работы с данными: useQuery, useMutation, usePaginatedQuery и другие хуки упрощают выполнение запросов и получение данных в компонентах React.

  2. Декларативный подход: при использовании RQ нет необходимости писать много кода для управления состоянием и выполнения запросов. Библиотека позволяет декларативно описывать данные, которые вам нужны, и предоставляет простые способы их получения и обновления, освобождая от рутинных задач.

  3. Встроенное кэширование: RQ автоматически кэширует полученные данные и обновляет кэш при необходимости. Это позволяет избежать повторных запросов к серверу и снижает нагрузку на сеть и сервер. Кроме того, кэшированные данные можно легко инвалидировать и обновлять при изменении данных на сервере, а так же отключить для тех запросов, где они не нужны.

  4. Умное управление состоянием: библиотека предоставляет удобные способы управления состоянием при выполнении асинхронных операций. RQ позволяет легко отслеживать статус запроса (загрузка, успех, ошибка), обрабатывать и отображать соответствующие состояния в пользовательском интерфейсе.

  5. Расширяемость: 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'

  • При повторном вызове начинаем всё сначала

    02f7feba6068a38fda5b869ceca1a51d.png

Схема работы 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.

© Habrahabr.ru