GraphQL в связке с TanStack Query используется как источник данных, где GraphQL отвечает за форму и структуру выборки, а TanStack Query — за кэширование, синхронизацию состояния сервера, повторные запросы, дедупликацию и управление жизненным циклом асинхронных операций.
В отличие от REST, GraphQL обычно работает через единый endpoint, что требует иной организации query keys, стратегии кэширования и разделения запросов на уровне клиента.
TanStack Query не зависит от способа получения данных. GraphQL-запросы оборачиваются в стандартные async-функции, возвращающие данные.
Основная идея — разделение ответственности:
Простейшая реализация через fetch:
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
});
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
return result.data;
}
Дальнейшее использование внутри useQuery:
import { useQuery } fr om '@tanstack/react-query';
const GET_USERS = `
query GetUsers {
users {
id
name
email
}
}
`;
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetchGraphQL(GET_USERS),
});
}
GraphQL не имеет URL-эндпоинтов как REST, поэтому структура ключей становится критически важной.
Основной принцип — ключ должен отражать:
Пример:
useQuery({
queryKey: ['users', { lim it: 10, offset: 0 }],
queryFn: () =>
fetchGraphQL(GET_USERS, { limit: 10, offset: 0 }),
});
Для сложных запросов ключ формируется детерминированно:
function usersKey(variables) {
return ['users', variables];
}
GraphQL активно использует variables, что хорошо сочетается с TanStack Query.
const GET_USER = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
posts {
id
title
}
}
}
`;
function useUser(id) {
return useQuery({
queryKey: ['user', id],
queryFn: () =>
fetchGraphQL(GET_USER, { id }),
enabled: Boolean(id),
});
}
Ключевой момент — синхронизация variables и queryKey. Несовпадение приводит к некорректному кэшированию.
Библиотека graphql-request упрощает выполнение запросов
и хорошо сочетается с TanStack Query.
import { request } from 'graphql-request';
const endpoint = 'https://api.example.com/graphql';
function fetcher(query, variables) {
return request(endpoint, query, variables);
}
Использование:
useQuery({
queryKey: ['posts'],
queryFn: () => fetcher(GET_POSTS),
});
Преимущество подхода — отсутствие ручной обработки
fetch, headers и JSON parsing.
TanStack Query и Apollo решают пересекающиеся задачи, но иногда используется гибридная модель:
Пример интеграции без замены Apollo cache:
import { useQuery as useApolloQuery } from '@apollo/client';
import { useQuery } from '@tanstack/react-query';
function useHybridUser(id) {
const apolloResult = useApolloQuery(GET_USER_APOLLO, { variables: { id } });
const tanstackResult = useQuery({
queryKey: ['user-extra', id],
queryFn: () => fetchExtraUserData(id),
});
return {
...apolloResult,
extra: tanstackResult.data,
};
}
Подход используется при постепенной миграции или разделении источников данных.
TanStack Query кэширует данные по queryKey, не по структуре GraphQL-запроса. Это важно, поскольку:
Это приводит к необходимости:
GraphQL мутации часто изменяют несколько сущностей одновременно.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables) =>
fetchGraphQL(UPDATE_USER, variables),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Инвалидация по префиксу ключа позволяет обновить связанные данные без ручного патчинга кэша.
GraphQL мутации хорошо сочетаются с optimistic updates, особенно при UI-интеракциях.
function useToggleLike() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (postId) =>
fetchGraphQL(TOGGLE_LIKE, { postId }),
onMutate: async (postId) => {
await queryClient.cancelQueries(['post', postId]);
const previous = queryClient.getQueryData(['post', postId]);
queryClient.setQueryData(['post', postId], (old) => ({
...old,
liked: !old.liked,
}));
return { previous };
},
onError: (err, postId, context) => {
queryClient.setQueryData(['post', postId], context.previous);
},
onSettled: (postId) => {
queryClient.invalidateQueries(['post', postId]);
},
});
}
GraphQL часто использует cursor-based pagination, что напрямую
поддерживается через useInfiniteQuery.
const GET_POSTS = `
query GetPosts($cursor: String) {
posts(after: $cursor, first: 10) {
edges {
node {
id
title
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`;
function usePosts() {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) =>
fetchGraphQL(GET_POSTS, { cursor: pageParam }),
initialPageParam: null,
getNextPageParam: (lastPage) =>
lastPage.posts.pageInfo.hasNextPage
? lastPage.posts.pageInfo.endCursor
: undefined,
});
}
TanStack Query не нормализует GraphQL данные по умолчанию. Это означает:
Решения:
queryClient.setQueryDataПример ручной синхронизации:
queryClient.setQueryData(['user', id], (old) => ({
...old,
name: 'Updated name',
}));
queryClient.setQueriesData(['posts'], (old) => {
return old?.map((p) =>
p.authorId === id ? { ...p, authorName: 'Updated name' } : p
);
});
GraphQL сам по себе агрегирует поля в один запрос, однако при использовании TanStack Query может возникать параллельный fetch нескольких queryFn.
Решение — использование request batching на уровне клиента:
TanStack Query при этом остаётся оркестратором, не изменяя транспортный уровень.
При серверном рендеринге GraphQL интеграция работает через prefetching:
await queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchGraphQL(GET_USER, { id }),
});
Далее состояние сериализуется:
dehydrate(queryClient);
На клиенте:
hydrate(queryClient, dehydratedState);
GraphQL-запросы при этом не отличаются от REST-версий — различие только в fetcher-функции.
Практическая структура проекта обычно строится вокруг операций:
Пример организации:
// api/graphql/queries/user.js
export const GET_USER = `...`;
// hooks/useUser.js
export function useUser(id) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchGraphQL(GET_USER, { id }),
});
}
При использовании TypeScript GraphQL-ответы часто генерируются через:
TanStack Query при этом типизирует только queryFn
результат:
function useUser(id: string) {
return useQuery<User>({
queryKey: ['user', id],
queryFn: () => fetchGraphQL(GET_USER, { id }),
});
}
Интеграция GraphQL и TanStack Query формирует архитектурный паттерн, где:
Такой подход делает систему предсказуемой, но требует строгой дисциплины в формировании queryKey и управлении инвалидацией.