Якщо ви використовуєте Next.js, Remix або Astro, ви хочете, щоб ваші дані завантажувалися на сервері для SEO та швидкого першого відтворення (LCP).
TanStack Query має для цього потужний механізм, який називається Hydration (Гідратація).
isPending: true). Користувач побачить "мерехтіння" (HTML з даними -> Loading -> HTML з даними).Нам треба передати "стан" з сервера на клієнт.
initialData (Простий, але обмежений)Ви можете передати дані як пропси.
// Next.js Page (Server Component або getServerSideProps)
export default async function Page() {
const posts = await getPosts();
// Передаємо posts в клієнтський компонент
return <PostsList initialPosts={posts} />;
}
// Client Component
'use client';
function PostsList({ initialPosts }) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: initialPosts, // Використовуємо дані з сервера
});
}
Мінуси:
PostsList знаходиться глибоко, це незручно.Цей метод дозволяє "попередньо заповнити" кеш QueryClient на сервері, а потім "відновити" його на клієнті.
В Next.js App Router це виглядає так:
getQueryClient (Singleton per request)На сервері ми не можемо мати один глобальний клієнт, бо запити різних користувачів змішаються. Нам потрібен новий клієнт для кожного запиту.
import { QueryClient } from '@tanstack/react-query';
import { cache } from 'react'; // React cache for memoization per request
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
export const getQueryClient = cache(() => createQueryClient());
Ми використовуємо HydrationBoundary та dehydrate.
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/get-query-client';
import { getPosts } from '@/api/posts';
import PostsList from './posts-list';
export default async function PostsPage() {
const queryClient = getQueryClient();
// 1. Prefetch даних на сервері
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
});
// 2. Дегідратація (перетворення стану в JSON)
const dehydratedState = dehydrate(queryClient);
return (
// 3. Передача стану в Boundary
<HydrationBoundary state={dehydratedState}>
<PostsList />
</HydrationBoundary>
);
}
Він нічого не знає про SSR. Він просто викликає useQuery.
'use client';
import { useQuery } from '@tanstack/react-query';
import { getPosts } from '@/api/posts';
export default function PostsList() {
// Цей хук миттєво поверне дані, бо вони вже є в HydrationBoundary!
// Ніякого loading state, ніякого мерехтіння.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
});
return <div>{data.map(p => <div key={p.id}>{p.title}</div>)}</div>;
}
Важливий нюанс: на сервері час виконання миттєвий. Але коли дані доходять до клієнта, проходить час.
Рекомендується встановлювати staleTime більше 0 (наприклад, 1 хвилину), щоб клієнт одразу після завантаження не почав робити refetch.
Якщо staleTime: 0 (default), то:
Це вбиває сенс SSR. Ставте staleTime: 60 * 1000.
Пам'ятайте, що дані, які передаються з сервера на клієнт (через dehydrate), повинні бути серіалізованими (JSON).
Date об'єкти перетворяться на рядки.Function, Map, Set не передадуться.Якщо вам потрібні Date об'єкти на клієнті, ви повинні відновлювати їх (парсити) всередині queryFn або select.
Архітектура та Best Practices
Ми вивчили API. Тепер поговоримо про те, як не перетворити проект на смітник. TanStack Query дає багато свободи, але "з великою силою приходить велика відповідальність".
Стратегії Тестування
Тестування асинхронного коду — це біль. "Flaky tests" (тести, що падають через раз), тайм-аути, проблеми з API...