Библиотека TanStack Query предназначена для управления асинхронным состоянием приложения. Основная задача — получение, кэширование, синхронизация и обновление серверных данных.
В обычном React-приложении работа с API часто приводит к следующим проблемам:
useEffectloadingТипичный код без TanStack Query:
import { useEffect, useState } from 'react'
function Users() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [])
if (loading) {
return <div>Загрузка...</div>
}
if (error) {
return <div>Ошибка</div>
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
)
}
При росте приложения подобный код начинает усложняться. Появляются:
TanStack Query решает эти задачи централизованно.
Для React используется пакет:
npm install @tanstack/react-query
Для работы Devtools:
npm install @tanstack/react-query-devtools
Центральным объектом библиотеки является
QueryClient.
Он отвечает за:
Создание клиента:
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient()
После создания клиента приложение оборачивается в провайдер.
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
QueryClient,
QueryClientProvider
} from '@tanstack/react-query'
import App from './App'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
QueryClientProvider делает клиент доступным во всем
дереве компонентов.
Основной хук библиотеки — useQuery.
Базовый синтаксис:
useQuery({
queryKey,
queryFn
})
Уникальный ключ запроса.
Функция получения данных.
import { useQuery } from '@tanstack/react-query'
function Users() {
const {
data,
isLoading,
error
} = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Ошибка загрузки')
}
return response.json()
}
})
if (isLoading) {
return <div>Загрузка...</div>
}
if (error) {
return <div>Ошибка</div>
}
return (
<ul>
{data.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
)
}
При первом рендере:
queryFnuseQuery возвращает большой объект состояния.
Наиболее используемые поля:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Полученные данные.
query.data
Объект ошибки.
query.error
Первичная загрузка.
query.isLoading
Флаг ошибки.
query.isError
Успешное получение данных.
query.isSuccess
Любой активный запрос.
query.isFetching
Ручной перезапрос.
query.refetch()
Это один из важнейших моментов в библиотеке.
Активен только при первой загрузке.
if (isLoading) {
return <Spinner />
}
Активен при любом запросе:
{isFetching && <small>Обновление...</small>}
queryKey — фундаментальная часть библиотеки.
Пример:
queryKey: ['users']
Ключ идентифицирует запрос в кэше.
Обычно используются массивы.
queryKey: ['user', userId]
queryKey: ['posts', category]
queryKey: ['products', filters]
Это позволяет:
function User({ userId }) {
const query = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
}
При изменении userId TanStack Query:
queryFn должна:
throwПравильный пример:
const fetchUsers = async () => {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Ошибка сервера')
}
return response.json()
}
Часто используется библиотека Axios.
import axios from 'axios'
const fetchUsers = async () => {
const response = await axios.get('/api/users')
return response.data
}
Использование:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Хорошая практика — выносить API в отдельные файлы.
import axios from 'axios'
export const fetchUsers = async () => {
const response = await axios.get('/api/users')
return response.data
}
import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from './api/users'
function Users() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
}
Преимущества:
После успешного запроса данные попадают в кэш.
Если другой компонент использует:
queryKey: ['users']
то повторный запрос не выполняется мгновенно.
Данные берутся из кэша.
По умолчанию данные считаются устаревшими сразу после получения.
Настройка:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 1000 * 60
})
Здесь данные будут считаться актуальными одну минуту.
Определяет, сколько хранить неиспользуемый кэш.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
gcTime: 1000 * 60 * 5
})
В новых версиях используется gcTime.
TanStack Query автоматически обновляет данные:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnWindowFocus: false
})
Отключает обновление при возврате на вкладку.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnReconnect: true
})
Обновляет данные после восстановления интернета.
По умолчанию библиотека автоматически повторяет неудачные запросы.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3
})
Задержка между попытками.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retryDelay: 1000
})
function Users() {
const {
data,
refetch
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
return (
<div>
<button onCl ick={() => refetch()}>
Обновить
</button>
{data?.map(user => (
<div key={user.id}>
{user.name}
</div>
))}
</div>
)
}
Можно использовать несколько useQuery.
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
Запросы выполняются параллельно.
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
enabled: !!userQuery.data
})
Второй запрос начнется только после первого.
Позволяет управлять запуском запроса.
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
enabled: Boolean(id)
})
const {
error,
isError
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
if (isError) {
return (
<div>
{error.message}
</div>
)
}
Временные данные до завершения запроса.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
placeholderData: []
})
Начальные данные для кэша.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
initialData: []
})
Подключение Devtools:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
Devtools позволяют:
Важно понимать различие между:
const [opened, setOpened] = useState(false)
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Серверные данные:
Именно для такого состояния создан TanStack Query.
Типичный жизненный цикл:
Наиболее распространенный шаблон:
const {
data,
isLoading,
isError,
error,
isFetching
} = useQuery({
queryKey: ['resource'],
queryFn: fetchResource,
staleTime: 1000 * 60
})
src/api/users.js
src/api/posts.js
src/hooks/useUsers.js
src/hooks/usePosts.js
src/components/Users.jsx
import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '../api/users'
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
}
Использование:
function Users() {
const { data } = useUsers()
}
Это позволяет:
Запросы переиспользуются между компонентами.
Несколько одинаковых запросов не создают лишний сетевой трафик.
Данные синхронизируются автоматически.
Встроенная система retry и error states.
Исчезает необходимость в большом количестве
useEffect.
Можно управлять: