TanStack Query - это библиотека для управления стейтом и кэширования данных в приложении React. Она предоставляет набор хуков, которые помогают работать с асинхронными запросами и кэшированием данных.
Вот перечень основных хуков TanStack Query:
useQuery
: позволяет получить данные из асинхронного источника (например, API) и автоматически кэширует их. Этот хук также обрабатывает ошибки и отменяет запросы при размонтировании компонента.useMutation
: позволяет отправлять асинхронные запросы для создания/обновления/удаления данных на сервере. Возвращает объект с методомmutate
, который можно вызвать для выполнения запроса.usePaginatedQuery
: позволяет загружать данные постранично. Возвращает объект со страницами данных и методами для переключения между страницами.useInfiniteQuery
: позволяет загружать данные пачками (например, при бесконечной подгрузке новых записей). Похож наusePaginatedQuery
, но загружает данные динамически по мере прокрутки.useQueryClient
: позволяет получить экземпляр клиента TanStack Query, который содержит информацию о кэшах, запросах и других внутренних состояниях.useIsFetching
: позволяет отслеживать количество активных запросов на странице.
Setup
Для начала нам нужно установить сам модуль и его девтулзы, которые позволят детально отследить его работу
npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
Далее нам нужно обернуть в данный модуль всё приложение, чтобы оно отслеживало запросы
Так же добавим ReactQueryDevtools
, который предоставит возможность отслеживать данные внутри приложения
index.ts
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();
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools />
</QueryClientProvider>
</React.StrictMode>,
);
Basic Example
Далее создадим приложение, которое будет выводить посты. Посты мы храним прямо в компоненте. Так же мы создали функцию wait()
, которая через определённое ожидание будет возвращать промис и выполнять определённое действие.
Первым делом мы запросим данные по постам через useQuery
, который принимает queryKey
(ключ для создания уникального запроса), queryFn
(функцию запроса данных с сервера)
Далее для изменения данных используется useMutation
, который принимает mutationFn
(функция отправки запроса и мутации данных на сервере) и свойство, которое будет выполнять логику при успешном запросе onSuccess
. В последнее свойство мы поместим метод клиента запросов invalidateQueries()
, который обновит пришедшие посты
App.tsx
import React from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
const posts = [
{ id: '1', title: 'post 1' },
{ id: '2', title: 'post 2' },
];
function App() {
// получаем возможность управлять клиентом
const queryClient = useQueryClient();
// запрос на получение
const postsQuery = useQuery({
// уникальный ключ запроса
queryKey: ['posts'],
// функция, которая будет выполняться при запросе
queryFn: () => wait(1000).then(() => [...posts]),
});
// запрос на создание нового поста
const createPostMutation = useMutation({
// функция мутации данных
mutationFn: (title: string) =>
wait(1000).then(() => posts.push({ id: crypto.randomUUID(), title: title })),
// при успешном запросе мы аннулируем данные, чтобы их перезагрузить
onSuccess: () => {
queryClient.invalidateQueries(['posts']);
},
});
if (postsQuery.isLoading) return <h1>Loading...</h1>;
if (postsQuery.isError) return <h1>Error 404</h1>;
return (
<div className='App'>
<h1>TanStack Query</h1>
<button
disabled={createPostMutation.isLoading}
onClick={() => createPostMutation.mutate('New Post')}
>
new post
</button>
<div>
{postsQuery.data.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
</div>
);
}
async function wait(duration: number) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
export default App;
Глобальные настройки
Глобальные настройки задаются внутри QueryClient
, который мы задаём в корневом компоненте. Мы можем задавать опции для queries
и mutations
.
_app.tsx
import { ReactQueryDevtools } from 'react-query/devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
function MyApp({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
useQuery
Первым делом стоит сказать, что функция запроса по умолчанию в себя принимает объект, который содержит некоторые метаданные и ключи, которые могут пригодиться в запросе
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: (obj) =>
wait(1000).then(() => {
console.log(obj);
return [...posts];
}),
});
Мы можем проверить статус так же здесь
Так же все запросы, на которые мы получили ответ, будут кешироваться в памяти и при перезагрузке страницы сначала отобразятся закешированные данные, а уже потом, в фоне, будут подгружаться новые данные, которые затем отобразятся
Чтобы данные оставались актуальными нужное нам количество времени (например, перезагружать список постов только раз в 5 минут, а не каждый раз при переходе на страницу), то можно указать, как для всего приложения время устаревания данных:
index.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
},
},
});
Так и указать время устаревания для отдельного хука запроса с помощью staleTime
:
App.tsx
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: (obj) =>
wait(1000).then(() => {
console.log(obj);
return [...posts];
}),
staleTime: 1000
});
Так же с помощью refetchInterval
мы можем явно указать раз в какое время нужно заново загружать данные:
App.tsx
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: (obj) =>
wait(1000).then(() => {
console.log(obj);
return [...posts];
}),
staleTime: 1000,
refetchInterval: 5000,
});
Свойство enabled
останавливает (если false
) или выполняет (если true
) запрос на получение данных. Может помочь, если требуется отображать данные, если подгрузились другие данные или просто совершать подгрузку по условию
Тут показаны самые частые данные, которые берутся из запроса
const { data, isError, isLoading, isSuccess, isFetching, refetch, status} = useQuery();
Настройки проекта, типизация с Typescript
Изначально стоит забиндить сервис, который будет работать с запросами в приложении
app > services > country.service.ts
import axios from 'axios'
// дефолтный url для запросов
const API_URL = 'http://localhost:3004'
// устанавливаем в аксиос базовый url для запросов
axios.defaults.baseURL = API_URL
// опишем интерфейс приходящих данных, который подхватится и TanStack Query
export interface ICountry {
id: number
title: string
population: string
image: string
}
// методы запросов
export const CountryService = {
async getAll() {
return axios.get<ICountry[]>('/countries')
},
async getById(id: string) {
return axios.get<ICountry>(`/countries/${id}`)
},
async create(data: ICountry) {
return axios.post('/countries', data, {
headers: { 'Content-Type': 'application/json' },
})
},
}
События onSuccess, onError
Так же события onSuccess
и onError
очень удобно использовать внутри query для реагирования на получение или на ошибку, так как эту логику взаимодействия мы описываем прямо внутри TanStack запросов
const [arrayCountries, setArrayCountries] = useState<ICountry[]>([]);
const { data } = useQuery(
'countries',
() => CountryService.getAll(), {
onSuccess: ({ data }) => {
setArrayCountries(data);
},
onError: (error) => alert(error?.message),
});
Трансформация данных (select)
select
позволяет изменить уже существующие данные и обработать их нужным для нас образом
const { isLoading, data: countries } = useQuery(
'country list',
() => CountryService.getAll(),
{
onError: (error: any) => {
alert(error.message)
},
select: ({ data }): ICountry[] =>
data.map(country => ({
...country,
title: country.title + ' !',
})),
}
)
Кастомный хук
Так же хорошей практикой является реализация хуков для получение определённых данных через react-query
app > hooks > useCountries.ts
import { useQuery } from 'react-query'
import { CountryService, ICountry } from '../services/country.service'
export const useCountries = () => {
const { isLoading, data: countries } = useQuery(
'country list',
() => CountryService.getAll(),
{
onError: (error: any) => {
alert(error.message)
},
select: ({ data }): ICountry[] =>
data.map(country => ({
...country,
title: country.title + ' !',
})),
}
)
return { isLoading, countries }
}
Использование:
pages > index.tsx
const Home: NextPage = () => {
const { isLoading, countries } = useCountries();
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>React Query</h1>
{isLoading ? (
<div>Loading...</div>
) : countries?.length ? (
<div className={styles.grid}>
{countries.map(country => (
<div className={styles.card} key={country.id}>
<Image
alt={country.title}
width={294}
height={208}
src={country.image}
/>
<h2>{country.title}</h2>
<p>
<b>Population:</b> {country.population}
</p>
</div>
))}
</div>
) : (
<div>Elements not found</div>
)}
</main>
</div>
);
};
Передать аргумент в useQuery (подгрузка по ID)
Уже таким образом будет выглядеть хук с передачей в него id
app > hooks > useCountry.ts
import { useQuery } from 'react-query'
import { CountryService, ICountry } from '../services/country.service'
// внутрь хука передаём id искомого элемента
export const useCountry = (id?: string) => {
const { isLoading, data: country } = useQuery(
// сюда передаём id вторым аргументом
['country list', id],
() => CountryService.getById(id || ''),
{
onError: (error: any) => {
alert(error.message)
},
select: ({ data }): ICountry => data,
// будем совершать поиск элемента только если у него есть id
enabled: !!id,
}
)
return { isLoading, country }
}
И так выглядит использование хука с передачей аргумента
pages > country > [id].tsx
import { NextPage } from 'next'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useCountry } from '../../app/hooks/useCountry'
import styles from '../../styles/Home.module.css'
const Country: NextPage = () => {
const { query } = useRouter()
const { country, isLoading } = useCountry(String(query?.id))
return (
<div className={styles.container}>
{isLoading ? (
<div>Loading...</div>
) : (
<main className={styles.main}>
<h1 className={styles.title}>{country?.title}</h1>
<div className={styles.grid}>
<div className={styles.card}>
<Image
alt={country?.title}
width={294}
height={208}
src={country?.image || ''}
/>
<h2>{country?.title}</h2>
<p>
<b>Population:</b> {country?.population}
</p>
</div>
</div>
</main>
)}
</div>
)
}
export default Country
GET запрос по кнопке “refetch”
Если мы хотим реализовать переполучение данных, чтобы пользователь сам запрашивал их по своему усмотрению, то из query можно вытащить метод refetch
, который можно передать в качестве onClick в кнопку
Devtools
Девтулзы позволяют нам запросить данные заново, сделать их неактуальными, сбросить или удалить - справа в блоке с самими данными. Так же они показывают свежие данные, запросы в данный момент времени, старые данные и неактивные запросы - слева вверху в блоке со всеми запросами
useMutation
Для совершения мутации используется хук useMutation
, который отправляет запросы на изменение данных на сервере
pages > create-country.tsx
import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useMutation } from 'react-query'
import styles from '../styles/Home.module.css'
import { CountryService, ICountry } from '../app/services/country.service'
const CreateCountry: NextPage = () => {
// начальные данные для формы
const [data, setData] = useState<ICountry>({
id: 5,
image: '/images/new-zeeland.jpeg',
population: '',
title: '',
} as ICountry);
// тут берём метод переадресации
const { push } = useRouter();
// тут мы пользуемся запросом на мутацию данных - добавление новой страны
const { isLoading, mutateAsync } = useMutation(
'create country',
// сюда мы можем спокойно передать нужные данные
(data: ICountry) => CountryService.create(data),
{
onSuccess: () => {
push('/'); // переадресация на главную
},
onError: (error: any) => {
alert(error.message);
},
}
);
// тут мы будем вызывать запрос на мутацию данных при отправке формы
const handleSubmit = async (e: any) => {
e.preventDefault()
await mutateAsync(data)
};
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>Create country</h1>
<div className={styles.grid}>
<div className={styles.card}>
<form onSubmit={handleSubmit}>
<input
placeholder='Enter id'
value={data.id}
onChange={e =>
setData({
...data,
id: +e.target.value,
})
}
/>
<input
placeholder='Enter image'
value={data.image}
onChange={e =>
setData({
...data,
image: e.target.value,
})
}
/>
<input
placeholder='Enter title'
value={data.title}
onChange={e =>
setData({
...data,
title: e.target.value,
})
}
/>
<input
placeholder='Enter population'
value={data.population}
onChange={e =>
setData({
...data,
population: e.target.value,
})
}
/>
{/* блокируем кнопку, пока идёт загрузка */}
<button disabled={isLoading}>Create</button>
</form>
</div>
</div>
</main>
</div>
)
}
export default CreateCountry