Практичный React Query

Практичный взгляд на React Query и его особенности

Cover Image

Оригинал - https://tkdodo.eu/blog/practical-react-query

Автор: https://github.com/tkdodo

Перевел: https://github.com/garbalau-github

Введение

Когда GraphQL и особенно Apollo Client обрели свою популярность в 2018, ходило много слухов о том, что эти технологии с легкостью заменят Redux, а вопрос Redux уже мертв? стал крайне популярным в интернете.

Я помню то время, когда совершенно не понимал ажиотаж вокруг. О чем все вокруг болтают? Я не мог понять, как библиотека для запросов может заменить state manager, вроде Redux. Как они вообще связаны?

Мне казалось что GraphQL и клиенты вроде Apollo, всего лишь будут запрашивать данные с сервера, как например это делает Axios для REST, а тебе лишь останется найти способ сделать эти данные доступные внутри приложения.

Но я еще никогда так не ошибался.

Состояние клиента и сервера

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

Это все звучит очень знакомо для тех разработчиков, кто использовал для таких целей Redux: запросить данные и сервера и сделать их повсеместно доступными

Выходит мы всегда относились к состоянию сервера также как и к состоянию клиента. Однако, есть все же одно отличие. Когда разговор идет о состоянии сервера - это то состояние, которое не принадлежит клиенту. Его просто нет в нашем приложении, на стороне клиента. Мы его будто бы одалживаем, чтобы показать самую свежую версию данных на экране пользователя. Именно сервер в данном случае является хранилищем данных.

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

Теперь я понял почему многие думают что Apollo сможет заменить Redux в множестве ситуаций.

React Query

Мне не удалось написать что-то на GraphQL

У нас есть работающее REST API, и у нас особо нет проблем с over-fetching, все работает нормально.

Видимо, не столкнулись мы еще с теми моментами, которые толкнули бы нас на перемены, и особенно когда тебе надо перестроить не только клиент но и серверный код, что не так уж и легко.

Однако, я все еще завидовал простоте того, как запросы данных работают на клиенте, включая обработку состояний загрузки (isLoading) и ошибок. Если бы в React для REST API было что-то подобное...

И оно есть, это - React Query.

React Query - это библиотека созданная Tanner Linsley в конце 2019 года. Эта библиотека взяла лучшее из Apollo и привнесла это в REST

React Query работает с любой функцией которая возвращает Promise, и использует стратегию кэширования stale-while-revalidate.

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

Кроме этого, библиотека также гибкая и позволяет модифицировать свои настройки, когда стандартных механизмов может не хватать.

Однако, не подумайте, эта статья не будет введением в React Query, я считаю, что официальная документация отлично подходит для описания основных концепций. Есть также видео с разных презентаций, и целый курс от самого создателя - фундаментальный курс, который отлично подойдет тем, кто захочет познакомиться с этой библиотекой поближе.

Я хочу больше сосредоточиться на некоторых практических советах, выходящих за рамки документации, которые могут быть полезны, когда вы уже работаете с библиотекой "на поле боя". Это то, что я усвоил за последние пару месяцев, когда не только активно использовал библиотеку на работе, но также принимал участие в жизни сообщества, отвечая на вопросы в Discord и в обсуждениях на Github.

Настройки по умолчанию

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

Прежде всего: React Query не вызывает queryFn при каждом повторном рендеринге, даже если staleTime по умолчанию равен нулю. Ваше приложение может перерисовываться по разным причинам в любое время, поэтому отправлением запросов каждый раз было бы безумием!

Если вы заметили что происходит refetch, которого вы не ожидали, скорее всего это из-за того что вы сделали фокус на window, и React Query решил вызвать refetchOnWindowFocus метод, который сам по себе отличная вещь в production. Почему? Если например пользователь переключит страницу в браузере, а потом снова вернется в ваше приложение, фоновый refetch будет запущен автоматически и данные на экране будут обновлены если что-то поменялось в это время на сервере.

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

Во время разработки, этот механизм будет срабатывать намного чаще, особенно когда вы например смотрите что-то в DevTools, а потом снова что-то нажимаете или фокусирует окно приложения, оно снова попытается сделать refetch, так что будьте в курсе такого поведения.

Я считаю было бы неплохо разъяснить разницу между cacheTime и staleTime, потому что эти понятия всплывают крайне часто, когда разговор идет о React Query

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

Cache Time: Продолжительность, по истечении которой неактивные запросы будут удалены из кеша. По умолчанию это 5 минут. Запросы переходят в неактивное состояние, как только не регистрируются слушатели, а именно, когда все компоненты, использующие этот запрос, не участвует в рендере.

В основном, если вам нужно будет поменять какие-то значения, скорее всего вы будете менять именно Stale Time, хоть опять же, всякое бывает, но мне никогда не приходилось вмешиватся в поведение Cache Time

Есть отличная статья об этом в документации

React Query DevTools

DevTools очень удобный, обязательно попробуйте!

React Query DevTools может помочь вам понять, в каком состоянии находится запрос. DevTools также сообщит вам, какие данные в данный момент находятся в кеше, что упростит отладку.

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

Массив зависимостей

Вы знакомы с useEffect?

Причем тут это? Потому что механизм массива зависимостей в useEffect и React Query похожи.

React Query будет запускать повторную загрузку всякий раз, когда изменяется ключ запроса. Это похоже на то, как useEffect отработает в том случае если какое-то занчение из его массива зависимостей изменится.

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

Вместо того чтобы организовывать сложные эффекты для ручного запуска повторного запроса, мы можем использовать ключ запроса:

type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>

const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}

export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state))

Представьте, что наш пользовательский интерфейс отображает список задач вместе с параметром фильтрации. Нам необходимо какое-то локальное состояние для того чтобы хранить состояние фильтра, и как только пользователь изменит свой выбор - мы обновим локальное состояние, и React Query автоматически запустит refetch для нас, потому что query key поменялся

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

Кстати, мне кажется я вообще никогда не посылал переменную в queryFn, которая не была бы частью query key.

Новая запись в кэше

Так как Query Key использован как ключ к Кэшу, мы получим новую запись в кэше, когда изменим фильтр, с например "All" на "Done", и это создаст состояние подргузки, когда пользователь поменяет значение в первый раз.

Это конечно же не идеально, так что нам остается использовать или keepPreviousData опцию для таких случаев, или, если это конечно возможно, сделать запись в только что созданном кэше с Initial Data.

type State = 'all' | 'open' | 'done';
type Todo = {
  id: number;
  state: State;
};
type Todos = ReadonlyArray<Todo>;

const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`);
  return response.data;
};

export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state), {
    initialData: () => {
      const allTodos = queryClient.getQueryData<Todos>(['todos', 'all']);
      const filteredData =
        allTodos?.filter((todo) => todo.state === state) ?? [];

      return filteredData.length > 0 ? filteredData : undefined;
    },
  });

Теперь, каждый раз когда пользователь переключается между состояниями, если у нас еще нет данных, мы попытаемся заранее записать ее с помощью нашего 'All' Todos кэша

Мы можем мгновенно показать 'Done' Todos, которые у нас есть, и они все равно увидит обновленный список когда фоновая загрузка закончиться

Обратите внимание, что до v3 вам также нужно было установить свойство initialStale, чтобы фактически запускать фоновую подгрузку.

Я думаю это отличный вклад в UX, и всего в несколько строчек кода

Разделяя серверное и клиентское состояние

Об этом хорошо сказано вот в этой вот статье - putting-props-to-use-state,

Если вы получаете данные из useQuery, постарайтесь не помещать эти данные в локальное состояние.

Основная причина в том, что вы неявно отказываетесь от всех фоновых обновлений, которые React Query делает за вас, потому что локальная копия состояния не будет обновляться.

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

Фоновые обновления вряд ли дадут что-то новое, даже если ваша форма уже инициализирована. Поэтому, если вы делаете это намеренно, убедитесь, что не отключены ненужные фоновые обновления, установив staleTime:

const App = () => {
  const { data } = useQuery('key', queryFn, { staleTime: Infinity })

  return data ? <MyForm initialData={data} /> : null
}

const MyForm = ({ initialData} ) => {
  const [data, setData] = React.useState(initialData)
  ...
}

Эту концепцию будет немного сложнее реализовать, когда вы отображаете данные, которые вы также хотите разрешить пользователю редактировать, но у него много преимуществ.

Запомните, что мы никогда не помещаем значение, полученное из React Query, в локальное состояние. Это гарантирует, что мы всегда видим самые свежие данные, потому что их локальная «копия» отсутствует.

Параметр 'enabled'

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

  • Извлекать данные в одном запросе и запускать второй запрос только после того, как мы успешно получили данные из первого запроса.

  • Включение и выключение запросов. У нас есть один запрос, который регулярно запрашивает данные благодаря refetchInterval, но мы можем временно остановить его, если модальное открыто, чтобы избежать обновления в задней части экрана.

  • Дождитесь ввода пользователя. Укажите некоторые критерии фильтрации в ключе запроса, но отключите их, пока пользователь не применит свои фильтры.

  • Отключить запрос после некоторого пользовательского ввода если у нас есть draft значение, которое должно иметь приоритет над данными сервера

Не используйте queryCache как менеджер состояний

Если вы вмешиваетесь в queryCache (queryClient.setQueryData), это должно быть только для оптимистичных обновлений, или для записи данных, которые вы получаете от сервера после мутации. Помните, что каждая фоновая загрузка может переопределить эти данные, поэтому используйте что-то другое для локального cостояния:

Hooks State

Zustand

Redux

Кастомные хуки

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

  • Вы можете вытащить фактическую загрузку данных из пользовательского интерфейса, но позже совместить с вашим вызовом useQuery.

  • Вы можете хранить все случаи использования одного ключа запроса (и, возможно, определения типов) в одном файле.

  • Если вам нужно настроить некоторые параметры или добавить преобразование данных, вы можете сделать это в одном месте.

Вы уже видели пример этого в секции про массив зависимостей.


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

P.S

Напомню, что эта статья является переводом статьи https://tkdodo.eu/blog/practical-react-query

Спасибо!