Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Эта статья — перевод оригинальной статьи "Common Patterns and Nuances Using React Query"
Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Из-за своей гибкой парадигмы React имеет несколько подходов проектирования проекта. Решения, которые мы принимаем на этапе проектирования и архитектуры проекта, могут либо сократить временные затраты на разработку простого надежного решения, либо затруднить её из-за усложнения реализации.
Одним из простых в реализации, но иногда сложных в использовании инструментов является react-query
— мощная библиотека для асинхронного управления состоянием. Простота реализации делает его желанным выбором для написания логики состояния компонентов.
Тем не менее, есть некоторые невысказанные аспекты React Query, которые могут показаться разочаровывающими и усложнить его, но часто выпадают из контекста проблемы, которую мы надеемся решить.
Следующие несколько концепций демонстрируют некоторые шаблоны для простых решений, обращая внимание на некоторые нюансы, которые могут возникнуть на этом пути.
Обратите внимание, что response-query
теперь называется TanStack Query, и эти концепции можно использовать в Vue, Solid и Svelte. В этой статье React он по-прежнему будет называться React Query (RQ).
Пробуем понять Query State Keys
Под капотом RQ выполняет некоторые сопоставления, аналогичные «типизированным» действиям, работающим в других конечных автоматах. Действия или, в нашем случае, запросы сопоставляются с ключом, значение которого равно null
или некоторому начальному состоянию (подробнее об этом в следующем разделе).
Поскольку RQ — это инструмент управления состоянием, состояние будет обновляться относительно других изменений состояния в компоненте. Это означает, что всякий раз, когда состояние компонента изменяется, эти изменения также влияют на состояние запроса, выполняющего проверки состояния, и соответственно обновляются.
Возьмите приложение Todo, которое загружает некоторые задачи. Снэпшот состояния запроса может выглядеть так:
{
queryKey: {
0: 'tasks
},
queryHash: "[\"tasks\"]"
}
Если ключ состояния изменяется, то же самое происходит и с кэшированными данными, доступными по ключу в этот момент. Изменения состояния будут происходить только для нескольких выбранных событий. Если ключ состояния не изменится, запрос все еще может выполняться, но кэшированные данные не будут обновляться.
Использование Initial и Placeholder Data
Примером этого является использование исходных данных для представления значений, которые не были полностью загружены. В конечных автоматах принято вводить начальное состояние перед гидратацией.
Возьмите приложение Todo ранее, которое должно показывать начальное состояние загрузки. При первоначальном запросе запрос задач находится в состоянии загрузки (он же isLoading = true
). UI отобразит содержимое на месте, когда оно будет готово. Это не лучший UX, но это можно быстро исправить.
RQ предоставляет опции для настроек initialData
или placeholderData
. Хотя эти свойства имеют сходство, разница заключается в том, где происходит кэширование: на Cache-level или на Observer-level.
Cache-level относится к кэшированию с помощью ключа запроса, где находятся начальные данные. Этот начальный кэш переопределяет кэши observer-level.
Observer-level относится к местоположению, в котором находится подписка, и где отображается placeholderData
. Данные на этом уровне не кэшируются и работают, если исходные данные не были кэшированы.
С InitialData
у вас больше контроля над staleTime
кеша и стратегиями повторной выборки. Принимая во внимание, что placeholderData
— допустимый вариант для простого улучшения UX. Имейте в виду, что состояния ошибок меняются в зависимости от выбора между кэшированием исходных данных или нет.
export default function TasksComponent() {
const { data, isPlaceholderData } = useQuery<TaskResponse>(
['tasks'],
getTasks,
{
placeholderData: {
tasks: [],
},
}
);
return (
<div>
{isPlaceholderData ? (
<div>Initial Placeholder</div>
) : (
<ul>
{data?.tasks?.map((task) => (
<li key={task.id}>{task.name}</li>
))}
</ul>
)}
</div>
);
}
Управление состоянием обновления
Расширяя предыдущую концепцию, обновление запроса происходит, когда либо состояние устарело, и мы контролируем обновление, либо мы используем другие механизмы, предоставляемые RQ, для выполнения тех же задач.
Регидратация данных
RQ предоставляет метод refresh
, который делает новый сетевой запрос для запроса и обновляет состояние. Однако, если кэшированное состояние такое же, как только что извлеченное, ничего не будет обновляться. Это может быть желательным результатом для ограничения обновлений состояния, но возьмем ситуацию, когда требуется ручная повторная выборка после возникновения мутации.
Использование ключа запроса с запросами на выборку повышает специфичность запроса и добавляет управление повторной выборкой. Это известно как Invalidation, когда данные ключа запроса помечаются как устаревшие, очищаются и повторно загружаются.
Повторная выборка данных может происходить автоматически или вручную в разной степени для любого из этих подходов. Это означает, что мы можем автоматически запросить данные повторной выборки по stateTime
или другим параметрам повторной выборки (refetchInterval
, refetchIntervalInBackground
, refetchOnMount
, refetchOnReconnect
и refetchOnWindowFocus
). И мы можем выполнить повторную загрузку вручную с помощью функции refetch
.
Автоматическое обновление довольно просто, но бывают случаи, когда мы хотим вручную инициировать обновление определенного ключа запроса, чтобы получить самые последние данные. Однако вы, вероятно, столкнётесь с ситуацией, когда функция refetch
не выполняет сетевой запрос.
Обработка HTTP ошибок
Одна из наиболее распространенных ситуаций, с которой сталкиваются новички в RQ, — это обработка ошибок, возвращаемых неудачными HTTP-запросами. В стандартном запросе на выборку с использованием useState
и useEffect
обычно создается некоторое состояние для управления сетевыми ошибками. Однако RQ может ловить ошибки из рантайма, если используемый API не содержит надлежащей обработки ошибок.
Будь то ошибка из рантайма или сетевая ошибка, они отображаются в свойстве error
запроса или мутации (также обозначаемой статусом isError
).
Чтобы справится с этим и передать сетевую ошибку в запрос, мы можем сделать одно или комбинацию этого:
сообщить fetch API, как обработать неудачный ответ
использование error boundaries с запросами и мутациями
Обработка ошибок с помощью Fetch
Для простого решения по обработке ошибок обработайте сетевые запросы от fetch API (или axios) следующим образом:
async function getTasks() {
try {
const data = await fetch('https://example.com/tasks');
// if data is possibly null
if(!data) {
throw new Error('No tasks found')
}
return data;
} catch (error) {
throw new Error('Something went wrong')
}
}
Затем в вашем компоненте:
export default function TasksComponent() {
const { data, isError } = useQuery<TaskResponse>(['tasks'], getTasks);
return (
<div>
{isError ? (
<>Unable to load errors at this time.</>
) : (
<ul>
{data?.tasks?.map((task) => (
<li key={task.id}>{task.name}</li>
))}
</ul>
)}
</div>
);
}
Этот шаблон повторяется, но он полезен для простых приложений, которые могут не требовать интенсивного использования запросов.
Обработка ошибок с помощью Error Boundaries
Возможно, лучшее, что разработчик React может сделать для своего приложения, — это установить Error Boundaries.
Error Boundaries помогают сдерживать ошибки рантайма, которые обычно приводят к сбою приложения, распространяясь по дереву компонентов. Однако они не могут обрабатывать сетевые ошибки без некоторой настройки.
К счастью, RQ делает это очень просто благодаря опции useErrorBoundary
:
const { data, isError } = useQuery<TaskResponse>(['tasks'], getTasks, { useErrorBoundary: true });
Хук принимает ошибку, кэширует ее и повторно выдает, поэтому Error Boundaries захватывает ее соответствующим образом.
Кроме того, передача в useErrorBoundary
функции, которая возвращает логическое значение, увеличивает детализацию обработки сетевых ошибок, например:
const { data, isError } = useQuery<TaskResponse>(['tasks'], getTasks, {
useErrorBoundary: (error) => error.response?.status >= 500
});
Выводы
Три основные концепции использования React Query:
Гидратация с помощью плэйсхолдеров или значений по умолчанию
Регидратация кеша свежими данными
обработка сетевых ошибок с правильной настройкой и error boundaries
Существует ряд инструментов управления состоянием, которые можно использовать с React, но React Query упрощает запуск и работу благодаря эффективному инструменту, который придерживается некоторых простых шаблонов и циклов рендеринга React.
Изучение того, как и когда выполнять запросы для достижения шаблона желания, требует некоторого понимания экосистемы React.