Настройка Apollo Client для GraphQL в веб-приложении
Apollo Client — полноценный GraphQL-клиент с нормализованным кэшем, реактивными запросами, мутациями, подписками и автогенерацией типов. Кэш хранит данные по идентификаторам объектов — обновление одной записи автоматически обновляет все компоненты, которые её используют.
Подходит для React, Vue, Angular — работает как отдельная библиотека. Особенно выигрывает в приложениях с большим числом взаимосвязанных объектов.
Что входит в работу
Установка и конфигурация Apollo Client, настройка HTTP и WebSocket link, аутентификация, кэш-политики, кодогенерация типов из схемы GraphQL, хуки для queries/mutations/subscriptions, обработка ошибок, DevTools.
Установка
npm install @apollo/client graphql
# для WebSocket
npm install graphql-ws
Конфигурация клиента
// lib/apollo/client.ts
import {
ApolloClient,
InMemoryCache,
createHttpLink,
from,
ApolloLink,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient as createWsClient } from 'graphql-ws'
import { getMainDefinition } from '@apollo/client/utilities'
import { split } from '@apollo/client'
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL ?? '/graphql',
})
// добавляем Authorization header к каждому запросу
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token')
return {
headers: {
...headers,
...(token ? { authorization: `Bearer ${token}` } : {}),
},
}
})
// обработка ошибок
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
for (const { message, locations, path, extensions } of graphQLErrors) {
console.error(`[GraphQL error]: ${message}`, { locations, path })
if (extensions?.code === 'UNAUTHENTICATED') {
authStore.logout()
}
}
}
if (networkError) {
console.error('[Network error]', networkError)
}
})
// WebSocket для подписок
const wsLink = new GraphQLWsLink(
createWsClient({
url: import.meta.env.VITE_GRAPHQL_WS_URL ?? 'ws://localhost:4000/graphql',
connectionParams: () => ({
authorization: `Bearer ${localStorage.getItem('token')}`,
}),
})
)
// split: subscriptions → WS, остальное → HTTP
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query)
return def.kind === 'OperationDefinition' && def.operation === 'subscription'
},
wsLink,
from([errorLink, authLink, httpLink])
)
export const apolloClient = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
Product: {
keyFields: ['id'],
},
// пагинация
Query: {
fields: {
products: {
keyArgs: ['categoryId', 'search'],
merge(existing, incoming, { args }) {
if (!args?.offset) return incoming
return {
...incoming,
items: [...(existing?.items ?? []), ...incoming.items],
}
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
},
})
// main.tsx
import { ApolloProvider } from '@apollo/client'
import { apolloClient } from '@/lib/apollo/client'
function App() {
return (
<ApolloProvider client={apolloClient}>
<Router />
</ApolloProvider>
)
}
Кодогенерация типов
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
npx graphql-codegen init
# codegen.ts или codegen.yml
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
overwrite: true,
schema: 'http://localhost:4000/graphql',
documents: 'src/**/*.graphql',
generates: {
'src/gql/': {
preset: 'client',
config: {
useTypeImports: true,
strictScalars: true,
scalars: {
DateTime: 'string',
UUID: 'string',
},
},
},
},
}
export default config
npx graphql-codegen --watch # в dev
npx graphql-codegen # в CI
GraphQL-документы
# src/features/products/products.graphql
query GetProducts($categoryId: ID!, $search: String, $offset: Int, $limit: Int) {
products(categoryId: $categoryId, search: $search, offset: $offset, limit: $limit) {
items {
id
name
price
stock
category {
id
name
}
}
total
hasMore
}
}
query GetProduct($id: ID!) {
product(id: $id) {
id
name
description
price
stock
images { url alt }
category { id name }
}
}
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
mutation UpdateProduct($id: ID!, $input: UpdateProductInput!) {
updateProduct(id: $id, input: $input) {
id
name
price
stock
}
}
subscription OnStockUpdate($productId: ID!) {
stockUpdated(productId: $productId) {
productId
stock
}
}
Хуки после кодогенерации
// features/products/useProducts.ts
import { useQuery, useMutation, useSubscription } from '@apollo/client'
import {
GetProductsDocument,
GetProductDocument,
CreateProductDocument,
UpdateProductDocument,
OnStockUpdateDocument,
} from '@/gql/graphql'
export function useProducts(categoryId: string) {
return useQuery(GetProductsDocument, {
variables: { categoryId },
notifyOnNetworkStatusChange: true,
})
}
export function useProduct(id: string) {
return useQuery(GetProductDocument, {
variables: { id },
skip: !id,
})
}
export function useCreateProduct() {
return useMutation(CreateProductDocument, {
update(cache, { data }) {
if (!data?.createProduct) return
cache.modify({
fields: {
products(existing) {
return {
...existing,
items: [data.createProduct, ...existing.items],
total: existing.total + 1,
}
},
},
})
},
})
}
export function useStockSubscription(productId: string) {
return useSubscription(OnStockUpdateDocument, {
variables: { productId },
onData: ({ client, data }) => {
const update = data.data?.stockUpdated
if (!update) return
client.cache.modify({
id: client.cache.identify({ __typename: 'Product', id: update.productId }),
fields: {
stock: () => update.stock,
},
})
},
})
}
Использование в компоненте
function ProductList({ categoryId }: { categoryId: string }) {
const { data, loading, error, fetchMore, networkStatus } = useProducts(categoryId)
const [createProduct, { loading: creating }] = useCreateProduct()
if (loading && networkStatus !== NetworkStatus.fetchMore) return <Skeleton />
if (error) return <ErrorMessage error={error} />
const products = data?.products.items ?? []
const hasMore = data?.products.hasMore ?? false
return (
<>
<ProductGrid products={products} />
{hasMore && (
<button
onClick={() => fetchMore({ variables: { offset: products.length } })}
disabled={networkStatus === NetworkStatus.fetchMore}
>
Загрузить ещё
</button>
)}
</>
)
}
Фрагменты для переиспользования
# fragments.graphql
fragment ProductBasic on Product {
id
name
price
stock
}
fragment ProductFull on Product {
...ProductBasic
description
images { url alt }
category { id name }
}
import { useFragment } from '@apollo/client'
import { ProductBasicFragmentDoc } from '@/gql/graphql'
// читаем фрагмент из кэша без отдельного запроса
function ProductCardFromCache({ id }: { id: string }) {
const { data } = useFragment({
fragment: ProductBasicFragmentDoc,
from: { __typename: 'Product', id },
})
return <div>{data?.name}</div>
}
Apollo DevTools
Расширение для Chrome/Firefox — визуализация кэша, просмотр всех выполненных запросов, мутаций и подписок, explorer для произвольных GraphQL-запросов прямо из браузера.
Что делаем
Настраиваем Apollo Client с HTTP + WS link, аутентификацией и обработкой ошибок, конфигурируем кодогенерацию типов, пишем запросы/мутации/подписки в .graphql файлах, настраиваем политики кэша для пагинированных коллекций, покрываем тестами через MockedProvider.
Срок: 3–5 дней, включая настройку кодогенерации и написание хуков для всех entities.







