Серверное состояние: TanStack Query
Серверные данные — это не то же самое, что клиентское состояние. Их нужно кешировать, обновлять, инвалидировать. Для этого есть TanStack Query (бывший React Query).
Проблема
Своими руками приходится делать:
- Кеш
- Индикатор загрузки
- Обработка ошибок
- Повторные запросы
- Инвалидация после мутаций
- Дедупликация запросов
TanStack Query даёт всё это из коробки.
Установка
npm install @tanstack/react-query
Провайдер
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Main />
</QueryClientProvider>
);
}
useQuery
Запрос данных:
import { useQuery } from '@tanstack/react-query';
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
if (!res.ok) throw new Error('Failed');
return res.json();
},
});
if (isLoading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка</p>;
return (
<ul>
{data.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
queryKey — идентификатор кеша. queryFn — функция запроса.
Параметры
useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
enabled: !!postId,
staleTime: 5 * 60 * 1000,
});
enabled— запускать ли запросstaleTime— сколько данные считаются свежимиrefetchOnWindowFocus— обновлять при возврате на вкладку
useMutation
Изменение данных:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost: PostInput) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
}).then((r) => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'Новый', body: 'Текст' })}
disabled={mutation.isPending}
>
Создать
</button>
);
}
invalidateQueries помечает кеш устаревшим и запускает повторный запрос.
Оптимистичное обновление
Обновить UI до ответа сервера:
useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previous = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old: Post[]) =>
old.map((p) => (p.id === newPost.id ? newPost : p))
);
return { previous };
},
onError: (_, __, context) => {
queryClient.setQueryData(['posts'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
SWR
Альтернатива от Vercel. Проще, но менее функциональна:
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) return <p>Загрузка</p>;
if (error) return <p>Ошибка</p>;
return <ul>{data.map((p: Post) => <li key={p.id}>{p.title}</li>)}</ul>;
}
Итоги
- Серверные данные — отдельный слой
- TanStack Query кеширует, повторяет, инвалидирует
useQueryдля чтения,useMutationдля записи- Оптимистичные обновления улучшают UX
- SWR — более простая альтернатива