В клиентских приложениях существует два фундаментально разных типа данных:
Локальное состояние полностью контролируется приложением: состояние модальных окон, активные вкладки, поля формы, переключатели интерфейса.
Серверное состояние принципиально отличается:
До появления TanStack Query разработчики обычно управляли серверными
данными вручную через useEffect, fetch,
axios, Redux, Context API или собственные сервисы. Это
приводило к большому количеству повторяющегося кода и сложной логике
синхронизации.
Пример типичного ручного запроса:
import { useEffect, useState } from 'react';
function UsersPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let active = true;
async function loadUsers() {
try {
setLoading(true);
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки');
}
const data = await response.json();
if (active) {
setUsers(data);
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
loadUsers();
return () => {
active = false;
};
}, []);
if (loading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка</div>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Даже такой простой пример уже содержит:
При росте приложения количество подобной логики начинает стремительно увеличиваться.
Одна из главных проблем традиционного подхода — постоянное дублирование одинаковой логики.
Практически каждый запрос требует:
loading;error;try/catch;useEffect;useState;Типичный пример:
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setIsLoading(true);
const response = await api.get();
setData(response);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
}
fetchData();
}, []);
В крупных проектах подобные конструкции могут повторяться сотни раз.
TanStack Query убирает этот шаблонный код:
const {
data,
isLoading,
error
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Библиотека автоматически:
Без специализированной библиотеки данные часто загружаются повторно даже тогда, когда они уже были получены ранее.
Например:
Проблемы такого подхода:
TanStack Query создаёт централизованный кэш серверных данных.
const query = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
После первой загрузки данные помещаются в кэш. При повторном открытии страницы библиотека может:
Без кэширования разные компоненты могут выполнять одинаковые HTTP-запросы одновременно.
Пример:
function Sidebar() {
const users = useUsers();
}
function Dashboard() {
const users = useUsers();
}
function Header() {
const users = useUsers();
}
Если useUsers() внутри вызывает обычный
fetch, сервер может получить три одинаковых запроса.
TanStack Query решает проблему дедупликации запросов.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Все компоненты используют общий query cache.
Результат:
Серверные данные могут изменяться в любой момент:
При ручном подходе интерфейс легко начинает отображать устаревшие данные.
TanStack Query предоставляет механизмы автоматической актуализации:
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 5000
});
Возможности:
Ручная реализация background refetch обычно очень сложна.
Необходимо учитывать:
TanStack Query автоматизирует фоновые обновления:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: true
});
Когда пользователь возвращается во вкладку браузера, библиотека автоматически проверяет актуальность данных.
Асинхронные запросы могут завершаться в непредсказуемом порядке.
Пример проблемы:
async function search(query) {
const result = await fetch(`/search?q=${query}`);
return result.json();
}
Если пользователь быстро вводит:
a
ab
abc
Запрос для a может завершиться позже запроса
abc.
Результат:
TanStack Query умеет:
В обычных приложениях обработка ошибок часто становится хаотичной:
try {
const data = await fetchData();
} catch (e) {
console.error(e);
}
Проблемы:
TanStack Query предоставляет встроенную систему обработки ошибок:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3
});
Возможности:
После изменения данных необходимо синхронизировать интерфейс.
Например:
await createTodo(todo);
После создания задачи требуется:
Без специальных инструментов разработчики часто вручную вызывают повторные запросы.
TanStack Query использует механизм invalidation:
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['todos']
});
}
});
После успешной мутации нужные запросы автоматически становятся невалидными и обновляются.
При обычном подходе пользователь вынужден ждать завершения запроса.
Например:
Это создаёт ощущение медленного интерфейса.
TanStack Query поддерживает optimistic updates:
useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
await queryClient.cancelQueries(['posts']);
const previousPosts = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], old => {
return old.map(post =>
post.id === newPost.id
? newPost
: post
);
});
return { previousPosts };
}
});
Интерфейс обновляется мгновенно ещё до ответа сервера.
При ручном управлении разные части интерфейса могут хранить собственные копии данных.
Проблемы:
TanStack Query использует единый query cache.
Все компоненты, использующие одинаковый queryKey,
автоматически получают одинаковые данные.
Обычные запросы требуют ручного контроля:
TanStack Query управляет жизненным циклом автоматически.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
enabled: isAuthenticated
});
Библиотека умеет:
Избыточные запросы и повторные рендеры ухудшают производительность.
Типичные причины:
TanStack Query оптимизирует работу приложения:
Ручная реализация пагинации обычно сопровождается большим количеством логики:
const [page, setPage] = useState(1);
const [items, setItems] = useState([]);
Необходимо учитывать:
TanStack Query предоставляет специализированные механизмы:
useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: lastPage => lastPage.nextCursor
});
Библиотека автоматически управляет:
При потере сети обычные приложения часто работают некорректно:
TanStack Query умеет:
По мере роста проекта ручное управление серверным состоянием становится всё сложнее.
Появляются:
TanStack Query создаёт единый стандартизированный подход к работе с серверными данными:
useQuery({...})
useMutation({...})
useInfiniteQuery({...})
Это позволяет:
Без специализированного инструмента разработчики часто помещают серверные данные в Redux или Context API.
Однако серверное состояние имеет совершенно другую природу:
| Клиентское состояние | Серверное состояние |
|---|---|
| полностью контролируется клиентом | контролируется сервером |
| синхронное | асинхронное |
| не требует кэширования | требует кэширования |
| локальное | удалённое |
| стабильное | может устаревать |
TanStack Query отделяет серверное состояние от UI-state.
Это делает архитектуру приложения значительно чище.
При изменении данных необходимо понимать:
TanStack Query предоставляет централизованную систему invalidation:
queryClient.invalidateQueries({
queryKey: ['users']
});
Инвалидация может быть:
При серверном рендеринге требуется:
TanStack Query поддерживает hydration/dehydration:
dehydrate(queryClient)
hydrate(queryClient, dehydratedState)
Это особенно важно для:
Традиционный код запросов часто строится императивно:
loadData();
reloadData();
fetchAgain();
TanStack Query использует декларативную модель:
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id)
});
Разработчик описывает:
Остальную работу библиотека берёт на себя.