Кэш или стэйт, пробуем React-query

от автора

Небо и море
Небо и море

Введение

Популярная библиотека для работы с состоянием веб-приложений  на react-js это redux.  Однако у нее есть ряд недостатков такие как многословность(даже в связке с redux-toolkit), необходимость выбирать дополнительный слой(redux-thunk, redux-saga, redux-observable). Возникает ощущение, что как-то это все слишком сложно и уже давно появились хуки и в частности хук useContext.. Так что я попробовал другое решение.

Приложение для теста 

У меня было простое веб приложение «Прогноз погоды» написанное с помощью create react app, typescript, redux-toolkit, redux saga. Потом я заменил весь redux на context + react-query. Это очень маленькое, однако рабочее приложение, которым я сам пользуюсь, позволило мне использовать react-query  для описания уже существующей логики. Т.е. не делать абстрактный нерабочий проект, который просто раскрывает базовые возможности библиотеки.. В приложении есть выбор городов, получение текущей погоды  и прогноза. Т.е. максимум три последовательных запроса к серверу.

Скрины тестового приложения
Скрины тестового приложения

Новый стэйт 

Библиотека react-query позволяет работать запросами к серверу, предоставляет доступ данным, позволяет задавать порядок запросов.. Однако для того чтобы с этим работать надо разделить весь стэйт который есть в redux на 2 части. Первая — это как раз данные, полученные с сервера. Вторая — это все остальное, в моем случае это города выбранные пользователем. 

Вторую часть реализовал с помощью react-context. Примерно так:

export const CitiesProvider = ({   children, }: {   children: React.ReactNode; }): JSX.Element => {   const [citiesState, setCitiesState] = useLocalStorage<CitiesState>(     'citiesState',     citiesStateInitValue,   );    const addCity = (id: number) => {     if (citiesState.citiesList.includes(id)) {       return;     }     setCitiesState(       (state: CitiesState): CitiesState => ({         ...state,         citiesList: [...citiesState.citiesList, id],       }),     );   };  // removeCity..,  setCurrentCity..    return (     <СitiesContext.Provider       value={{         currentCity: citiesState.currentCity,         cities: citiesState.citiesList,         addCity,         removeCity,         setCurrentCity,       }}     >       {children}     </СitiesContext.Provider>   ); }; 

Реализация не сложная, методы setCurrentCity, removeCity аналогичны. Также я написал хук, который сохраняет все в localStorage при изменении стэйта и подключил его к провайдеру. В итоге у пользователя есть список выбранных городов, текущий город, и возможность удалять, добавлять, выбирать текущий.

React-query

Для загрузки, хранения, обновления данных с сервера использовал библиотеку react-query. Подключается  примерно так:

import { QueryClient, QueryClientProvider } from 'react-query'; import { ReactQueryDevtools } from 'react-query/devtools';   import { CitiesProvider } from './store/cities/cities-provider';   const queryClient = new QueryClient();   ReactDOM.render(   <React.StrictMode>     <QueryClientProvider client={queryClient}>       <CitiesProvider>         <App />

Простой пример использования:

const queryCities = useQuery('cities', fetchCitiesFunc); const cities = queryCities.data || [];

Первый параметр 'cities' это ключ строка, которая должна быть уникальной для каждого запроса. Второй — это функция, которая возвращает Promise, который резолвит данные или отдает ошибку. Также можно передать третьим параметром объект с настройками.

useQuery возвращает объект UseQueryResult, который содержит данные о состоянии запроса, ошибку или данные

const { isLoading, isIdle, isError, data, error } = useQuery(..

Для выполнения последовательных запросов мне показалось удобным написать отдельный хук

export function useCurrentWeather(): WeatherCache {   const { currentCity } = useContext(СitiesContext);    // запрашиваем список городов   const queryCities = useQuery('cities', fetchCitiesFunc, {      refetchOnWindowFocus: false,     staleTime: 1000 * 60 * 1000,   });   const citiesRu = queryCities.data || [];   // ищем идентификатор текущего города..   const city = citiesRu.find((city) => {     if (city === undefined) return false;     const { id: elId } = city;     if (currentCity === elId) return true;     return false;   });     const { id: weatherId } = city ?? {};    // запрашиваем текущую погоду   const queryWeatherCity = useQuery(     ['weatherCity', weatherId],     () => fetchWeatherCityApi(weatherId as number),     {       enabled: !!weatherId,       staleTime: 5 * 60 * 1000,     },   );     const { coord } = queryWeatherCity.data ?? {};    // запрашиваем прогноз по координатам из предыд. запроса   const queryForecastCity = useQuery(     ['forecastCity', coord],     () => fetchForecastCityApi(coord as Coord),     {       enabled: !!coord,       staleTime: 5 * 60 * 1000,     },   );     return {     city,     queryWeatherCity,     queryForecastCity,   }; }

staleTime — Время, по истечении которого, данные считаются устаревшими. Устаревшие данные перезапрашиваются автоматически при монтировании нового экземпляра, перефокусировке или переподключении сети. Интересно, что по умолчанию staleTime =0.

 enabled: !!weatherId, Эта настройка позволяет выполнять запрос только при определенном условии. Пока условие не будет выполнено useQuery будет возвращать состояние isIdle. Таким образом можно описать последовательность выполнения запросов.

const queryWeatherCity = useQuery(['weatherCity', weatherId],.. 

Ключ может быть как строкой так и массивом, содержащим строку и неограниченное количество сериализуемых объектов, например строка + идентификатор.

Вот так использую этот хук в компоненте:

export function Forecast(): React.ReactElement {   const {     queryForecastCity: { isFetching, isLoading, isIdle, data: forecast },   } = useCurrentWeather();     if (isIdle) return <LoadingInfo text="Ожидание загрузки дневного прогноза" />;   if (isLoading) return <LoadingInfo text="Загружается дневной прогноз" />;     const { daily = [], alerts = [], hourly = [] } = forecast ?? {};   const dailyForecastNext = daily.slice(1) || [];     return (     <>       <Alerts alerts={alerts} />       <HourlyForecast hourlyForecast={hourly} />       <DailyForecast dailyForecast={dailyForecastNext} />       {isFetching && <LoadingInfo text="Обновляется дневной прогноз" />}     </>   ); }

Есть два разных состояния isLoading — это первая загрузка и isFetching — это обновление.

Инструменты разработчика

У React-query есть возможность вывести окошко инструментов разработчика. Оно немного похоже на окно Redux, но появляется в виде фиксированного окошка поверх приложения(можно закрыть и останется только кнопка) 

Окно инструментов разработчика
Окно инструментов разработчика

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

import { ReactQueryDevtools } from 'react-query/devtools';

В документации сказано, что при process.env.NODE_ENV === 'production' ,  в релизную сборку это не попадет автоматически. У меня в Create React App все корректно.

Другие возможности

Также у react-query есть возможности, которые мне не понадобились, однако я все же опишу некоторые из них, примеры кода будут из документации.

  • useQueries позволяет динамически формировать массив запросов. Это нужно т.к. мы не можем опционально вызывать хуки useQuery.

const userQueries = useQueries(   users.map(user => {     return {       queryKey: ['user', user.id],       queryFn: () => fetchUserById(user.id),     }   })
  • По умолчанию настроен автоматический перезапрос данных, при получении ошибки, 3 попытки. Это можно настроить с помощью конфига retry.

  • Для запросов на создание, обновление, удаление данных есть хук useMutations

const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
  • Можно делать постраничные запросы, для бесконечных запросов есть хук useInfiniteQuery

  • Также есть предзагрузка, инвалидация запросов, оптимистичное обновление и еще много всего, что можно посмотреть в документации.

Заключение

После замены redux-toolkit + redux-saga и context + react-query код мне показался значительно проще и я получил из коробки больший функционал для работы с запросами к серверу.  Однако часть с react-context не имеет специальных инструментов отладки и вообще вызывает опасения, но она оказалось совсем небольшой и мне вполне хватило react-devtools. В целом я доволен библиотекой react-query и вообще идея отделения кэша в отдельную сущность кажется мне интересной. Но все же это очень маленькое приложение с несколькими get запросами..   

Ссылки

Верстка корректна только для мобильных устройств

Есть ветка с redux

Документация react-query

ссылка на оригинал статьи https://habr.com/ru/post/557620/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *