Налаштування Apollo Client для GraphQL у веб-додатках
Apollo Client — повноцінний GraphQL-клієнт з нормалізованим кешем, реактивними запитами, мутаціями, підписками та автогенерацією типів. Кеш зберігає дані за ідентифікаторами об'єктів — оновлення одного запису автоматично оновлює всі компоненти, які його використовують.
Підходить для React, Vue, Angular — працює як окрема бібліотека. Особливо виигрує в додатках з великою кількістю взаємопов'язаних об'єктів.
Що входить у роботу
Встановлення та конфігурація Apollo Client, налаштування HTTP та WebSocket link, аутентифікація, кеш-політики, кодогенерація типів зі схеми GraphQL, хуки для запитів/мутацій/підписок, обробка помилок, 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.







