Всем привет! Меня зовут Андрей Демьянов. Я тимлид и разработчик в МТС Travel. Совместно с другими командами мы создаем сервис по бронированию отелей в России и всему миру. Развиваемся с нуля, поэтому прямо на себе испытываем необходимость в новых библиотеках, подходах и изменениях, которые связаны с расширением и улучшением возможностей.

В этой статье хочу рассказать о нашем опыте работы с библиотекой React Query (ныне TanStack Query, дальше RQ) и почему мы остановились именно на ней. А еще как она помогает нам упростить и ускорить доступ к страницам и данным, сэкономить ресурсы на однотипных запросах, упростить визуализацию работы с данными и распутать код. Глубоких технических подробностей не будет, но статья может быть интересна тем, кто хочет узнать об опыте применения RQ в условиях продуктовой разработки.

Как мы выбирали библиотеку

При закладывании первичной архитектуры мы решили отталкиваться от потребностей в хранении и обработке данных на стороне клиента, не поддаваясь при этом соблазнам и внутренним голосам.

Первым инструментом для нас стал MobX. Он закрывал список базовых требований: хранить, обрабатывать и переиспользовать результаты ответов бэкенда. Но MobX не соответствовал требованиям, которые у нас появились к нему в будущем — например, нам стало не хватать встроенного кэширования для типовых запросов и возможности контролировать весь цикл работы с данными для клиента.

Мы прожили с MobX несколько месяцев, но достигнутых результатов не хватало для того, чтобы почувствовать удовлетворение и остановиться в поисках. Мы стали хранить часть данных на клиенте, но значительного роста производительности и скорости это не дало.

Однотипных запросов на поиск становилось больше, продакты продолжали приходить с новыми фичами по расширению функционала страниц, а продукт определился с курсом развития. Теперь пользователь должен был получить возможность быстро перемещаться между поисковой выдачей и страницей с данными об отеле. При этом желательно, чтобы это происходило максимально быстро и в процессе никто не заскучал.

После этих событий стали понятны запросы, с которыми мы выходим на рынок библиотек:

  • Мы не могли настроить удобный переход со страницы с подробностями обратно на страницу поиска.

  • Что делать, если нам не хочется заниматься props drilling, но нужно иметь возможность получать данные на разных уровнях вложенности? И при этом не обрасти однотипным кодом? Поясню, что props drilling — это процесс передачи свойств (props) через множество уровней вложенных компонентов в React, даже если промежуточные компоненты не используют эти свойства напрямую. Это может приводить к раздуванию кода, снижению его читабельности и управляемости, к возникновению лишних перерисовок. Все это негативно сказывается на производительности приложения.

  • Как бы нам получать состояния загрузки, чтобы не травмировать пользователя? Чтобы это было удобно и эффективно?

Мы сформировали требования и поняли, что лучший вариант — React Query.

Что такое React Query

React Query (или TanStack Query) — библиотека для работы с асинхронными операциями, обновления их в фоне и синхронизации состояний. Она избавляет команду от необходимости писать повторяющийся код и управлять состоянием вручную. По сути React Query — это набор хуков и утилит для работы с асинхронными запросами, кэшированием и синхронизацией данных с сервером.

Одно из главных преимуществ RQ — его легкость в интеграции и возможность выбирать только те функции, которые нужны для конкретного проекта. Библиотека упрощает загрузку, обновление и кэширование данных, делает эти процессы более эффективными и предсказуемыми. К тому же облегчает работу с ошибками и реализацию оптимистичного UI. А значит, приложения становятся более отзывчивыми и удобными для пользователей.

Как библиотеку сравнивали с конкурентами, можно посмотреть на официальном сайте. Там же есть сравнение размеров бандла, скорости, поддержки query-синтаксисов и даже количества звездочек на гитхабе.

Наши проблемы и попытки их решения

Основной проблемой для нас стала необходимость кэширования запросов с непредсказуемым временем ожидания. Длительные задержки при переходах между страницами приводили к тому, что пользователи покидали сервис или у них был негативный опыт его использования.

Чтобы решить проблему, мы воспользовались несколькими механиками RQ:

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

  • Повтор запросов через определенное время. Это свойство RQ позволяет автоматизировать актуализацию данных на странице, не прибегая к написанию лишнего кода. При этом от нас требуется только в явном виде передать, через сколько потребуется сделать новый запрос.

  • Передача статусов хода загрузки. Это коллекция флагов, которые позволяют тонко настраивать визуализацию хода загрузки для пользователя.

Мы провели инициализацию в корне проекта и создали корневой провайдер. Она состояла из двух обязательных импортов — QueryClient и QueryClientProvider. Первый из них — это экземпляр объекта. Он задействован в момент инициализации переменной queryClient. Второй используется в качестве провайдера, получающего в поле client наш queryClient. Все это поможет в дальнейшем получать данные, хранящиеся в RQ, для всех мест, которые оказались обернуты импортированным QueryClientProvider.

Вместе с тем мы импортировали и созданный нами кастомный хук для кэширования RQ в IndexedDB, вызываемый после рендера страницы (почитать о методе persistQueryClint можно в документации):

Дальше нам требовалось создать новый хук useQuery, который аккумулировал бы нашу работу с данными и позволил бы обращаться к результатам запросов только там, где это нужно.

Первым делом тут требуется функция, которая выполнит роль генератора ключа. Ключ объединяет параметры поисковой выдачи, которые мы считаем уникальными — это, например, дата, количество гостей, выбранные фильтры и так далее.

Ключ в RQ — это массив из двух элементов. Первый — это некое название, для которого мы далее храним значения. Второй — уникальная строка, позволяющая понять, нужно нам достать результат из кэша или сделать повторный запрос.

После создания ключа мы готовы инициализировать кастомный хук, который поможет справиться со всеми пожеланиями.

Объект response — это коллекция множества различных статусов о запросе:

  • классические isLoading (процесс загрузки) и isSuccess (данные готовы к работе);

  • isLoadingError — обработка ошибки при загрузке;

  • isFetching — визуализация только времени ожидания ответа;

  • isError — ошибка;

  • dataUpdatedAt — узнать время обновления данных и так далее.

Коротко о дедупликации

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

Тут можно отметить два момента:

  1. Чтобы повторить запрос к данным, нужно обновить свойства (properties), которые мы передаем в хук. Любое их изменение приведет к попаданию в цикл RQ — это когда мы проверяем наличие закэшированного ответа и возвращаем ранее вычисленный результат или делаем новое обращение к серверу. Поэтому внимательно отнеситесь к тому, что должно быть триггером к изменениям.

  2. Мы получаем данные только в конкретном месте, а значит у нас нет необходимости протаскивать результат запроса сквозь толщу компонентов (вызывая props drilling). Мы можем точечно вызывать наш хук, передавая в него лишь пару нужных пропов. Это позволит нам избежать лишних ререндеров, сделать код проще и читабельнее, добавит предсказуемость в ответах.

Финальное

Простота управления состояниями запросов к серверу, гибкое кэширование запроса и скромное количество кода дали нам несколько возможностей:

  • быстро и эффективно показывать однотипные страницы с большим количеством данных. Например, если пользователь постоянно переходит на отель и возвращается к выдаче результатов;

  • обновлять данные по таймауту, показывать нужные ошибки и визуализации загрузок данных;

  • легко получать доступ к тонким настройкам.

А еще мы не переживаем, что количество кода монструозно увеличивается с каждой хотелкой.

После года использования библиотеки мы все еще не уверены, что знаем все ее секреты. Но точно можем сказать, что RQ помог нам закрыть все возникающие потребности на этапе взаимодействия с однотипными запросами.

Спасибо за внимание!

Комментарии (6)


  1. kellas
    11.07.2024 22:12
    +2

    Неплохо! Хоть и можно было в данном случае информацию об отеле просто в "модалке" выводить и не рендерить каждый раз страницу результатов поиска при смене роута, а просто переключать свойство display, а для остального service-worker.

    Но за статью спасибо, не знал о таких возможностях tanstack , надо будет доку получше почитать.


  1. sumdy-c
    11.07.2024 22:12
    +1

    Статью не читал, потому что юзаю RQ на постоянстве.

    *Интересный момент - при сильно связанной с бэком архитектуре, RQ (частично конечно), в достаточной степени заменяет RTK или MST. Иногда настолько, что я просто не подключаю глобальное состояние, а обновляю компонент с помощью кэша запроса. Имба короче - пользуйтесь.