Интеграция с GraphQL

GraphQL в связке с TanStack Query используется как источник данных, где GraphQL отвечает за форму и структуру выборки, а TanStack Query — за кэширование, синхронизацию состояния сервера, повторные запросы, дедупликацию и управление жизненным циклом асинхронных операций.

В отличие от REST, GraphQL обычно работает через единый endpoint, что требует иной организации query keys, стратегии кэширования и разделения запросов на уровне клиента.


Базовая модель интеграции

TanStack Query не зависит от способа получения данных. GraphQL-запросы оборачиваются в стандартные async-функции, возвращающие данные.

Основная идея — разделение ответственности:

  • GraphQL клиент выполняет запрос
  • TanStack Query управляет состоянием запроса

Простейшая реализация через 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),
  });
}

Формирование query keys для GraphQL

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

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

Библиотека 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.


Интеграция с Apollo Client (частичная)

TanStack Query и Apollo решают пересекающиеся задачи, но иногда используется гибридная модель:

  • Apollo — для cache-aware GraphQL
  • TanStack Query — для независимого server-state или REST/GraphQL микса

Пример интеграции без замены 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,
  };
}

Подход используется при постепенной миграции или разделении источников данных.


Кэширование GraphQL в TanStack Query

TanStack Query кэширует данные по queryKey, не по структуре GraphQL-запроса. Это важно, поскольку:

  • одинаковые запросы с разными variables считаются разными ключами
  • разные запросы с одинаковыми данными не объединяются автоматически

Это приводит к необходимости:

  • строгой нормализации queryKey
  • явного контроля инвалидирования

Инвалидация кэша после мутаций

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

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,
  });
}

Нормализация данных и отсутствие встроенного cache graph

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-запросов

GraphQL сам по себе агрегирует поля в один запрос, однако при использовании TanStack Query может возникать параллельный fetch нескольких queryFn.

Решение — использование request batching на уровне клиента:

  • Apollo Link BatchHttpLink
  • graphql-request batching (кастомная реализация)
  • DataLoader на сервере

TanStack Query при этом остаётся оркестратором, не изменяя транспортный уровень.


SSR и GraphQL hydration

При серверном рендеринге GraphQL интеграция работает через prefetching:

await queryClient.prefetchQuery({
  queryKey: ['user', id],
  queryFn: () => fetchGraphQL(GET_USER, { id }),
});

Далее состояние сериализуется:

dehydrate(queryClient);

На клиенте:

hydrate(queryClient, dehydratedState);

GraphQL-запросы при этом не отличаются от REST-версий — различие только в fetcher-функции.


Разделение GraphQL операций

Практическая структура проекта обычно строится вокруг операций:

  • queries
  • mutations
  • fragments (если используется GraphQL клиент уровня Apollo-подобного)

Пример организации:

// 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-ответы часто генерируются через:

  • GraphQL Code Generator
  • typed document nodes

TanStack Query при этом типизирует только queryFn результат:

function useUser(id: string) {
  return useQuery<User>({
    queryKey: ['user', id],
    queryFn: () => fetchGraphQL(GET_USER, { id }),
  });
}

Особенности архитектуры при GraphQL

Интеграция GraphQL и TanStack Query формирует архитектурный паттерн, где:

  • GraphQL отвечает за структуру данных
  • TanStack Query отвечает за состояние, синхронизацию и кэш
  • нормализация остаётся на уровне разработчика
  • ключи запросов становятся основным источником консистентности

Такой подход делает систему предсказуемой, но требует строгой дисциплины в формировании queryKey и управлении инвалидацией.