Apollo Client ile React Uygulamasına GraphQL Entegrasyonu
GraphQL’i ilk öğrendiğimde, REST API’lere göre ne kadar büyük bir zihniyet değişikliği gerektirdiğini fark etmem biraz zaman aldı. Ama Apollo Client ile React’i bir araya getirdiğimde, “neden bu kadar geç kaldım?” dedim açıkçası. Eğer siz de hâlâ her component için ayrı ayrı useEffect + fetch yazıyorsanız ve loading state’lerini elle yönetmekten bıktıysanız, bu yazı tam size göre.
Apollo Client Nedir ve Neden Kullanmalıyız?
Apollo Client, sadece bir HTTP istemcisi değil. Cache yönetimi, reactive variables, optimistic updates, error handling… Bunların hepsini tek bir paket altında sunuyor. Düşünün ki her GraphQL sorgusundan dönen veriyi otomatik olarak normalize edip cache’e yazıyor, aynı veriyi bir daha sunucudan çekmeden direkt cache’den sunuyor.
REST dünyasında bunu kendiniz yazmak için ya Redux + custom middleware kuruyordunuz ya da React Query gibi bir kütüphane ekliyordunuz. Apollo, GraphQL ekosistemi için bunu neredeyse kutudan çıkar çıkmaz sağlıyor.
Büyük bir e-ticaret projesinde çalışırken, ürün detay sayfasına her gidip döndüğümüzde API’ye tekrar istek atıldığını fark ettik. Apollo’nun cache mekanizmasını doğru configure ettiğimizde, aynı ürün için saniyeler içinde ikinci bir network isteği gitmiyordu. Kullanıcı deneyimi ciddi ölçüde iyileşti.
Kurulum ve Temel Yapılandırma
Önce gerekli paketleri kuralım:
npm install @apollo/client graphql
# ya da yarn kullanıyorsanız
yarn add @apollo/client graphql
Burada graphql paketi Apollo’nun peer dependency’si, atlamayın. Şimdi Apollo Client’ı oluşturup uygulamamıza bağlayalım:
# src/apollo/client.ts dosyasını oluşturalım
// src/apollo/client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: process.env.REACT_APP_GRAPHQL_URL || 'http://localhost:4000/graphql',
});
// Auth token'ı her isteğe ekleyen middleware
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
export const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Product: {
keyFields: ['id', 'sku'], // Cache key olarak id ve sku kullan
},
},
}),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
},
},
});
typePolicies kısmı çoğu kişinin atlayıp sonradan pişman olduğu bir detay. Eğer backend’iniz cache’i doğru normalize edemiyorsa, aynı ürünü farklı query’lerden getirdiğinizde cache’de iki ayrı kayıt oluşabilir. Bu da stale data sorunlarına yol açar.
Şimdi bu client’ı React uygulamamıza bağlayalım:
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import { client } from './apollo/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);
ApolloProvider Context API üzerinden çalıştığı için, bu wrapper’ın altındaki her component useQuery, useMutation gibi hook’lara erişebilir.
useQuery Hook ile Veri Çekme
En sık kullanacağınız hook bu olacak. Basit bir ürün listesi örneğiyle başlayalım:
// src/components/ProductList.tsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCTS = gql`
query GetProducts($category: String, $limit: Int) {
products(category: $category, limit: $limit) {
id
sku
name
price
stock
category {
id
name
}
images {
url
alt
}
}
}
`;
interface Product {
id: string;
sku: string;
name: string;
price: number;
stock: number;
category: { id: string; name: string };
images: { url: string; alt: string }[];
}
const ProductList: React.FC<{ category?: string }> = ({ category }) => {
const { loading, error, data, refetch } = useQuery(GET_PRODUCTS, {
variables: {
category,
limit: 20,
},
fetchPolicy: 'cache-and-network', // Cache'i göster, arka planda güncelle
notifyOnNetworkStatusChange: true,
});
if (loading && !data) return <div>Yükleniyor...</div>;
if (error) {
console.error('GraphQL Error:', error);
return (
<div>
<p>Ürünler yüklenemedi: {error.message}</p>
<button onClick={() => refetch()}>Tekrar Dene</button>
</div>
);
}
const products: Product[] = data?.products || [];
return (
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<img src={product.images[0]?.url} alt={product.images[0]?.alt} />
<h3>{product.name}</h3>
<p>{product.price} TL</p>
<span className={product.stock > 0 ? 'in-stock' : 'out-of-stock'}>
{product.stock > 0 ? 'Stokta var' : 'Tükendi'}
</span>
</div>
))}
</div>
);
};
export default ProductList;
fetchPolicy seçenekleri konusunda biraz duralım çünkü bu kararı yanlış verirseniz ya çok fazla network isteği atarsınız ya da kullanıcıya bayat veri gösterirsiniz:
- cache-first: Önce cache’e bak, varsa network’e gitme. Default değer bu.
- cache-and-network: Cache’deki veriyi hemen göster, arka planda sunucudan güncelle.
- network-only: Her seferinde sunucudan çek, cache’i güncelle ama okuma.
- no-cache: Sunucudan çek, cache’e bile yazma.
- cache-only: Sadece cache’den oku, hata olsa bile network’e gitme.
Gerçek dünya senaryosunda genellikle cache-and-network kullanmak en iyi kullanıcı deneyimini sağlıyor. Sayfa hızlı yükleniyor, arka planda da taze veri geliyor.
useMutation ile Veri Yazma
Query’leri hallettik, şimdi mutation’lara bakalım. Sepete ürün ekleme örneği üzerinden gidelim:
// src/components/AddToCartButton.tsx
import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';
const ADD_TO_CART = gql`
mutation AddToCart($productId: ID!, $quantity: Int!) {
addToCart(productId: $productId, quantity: $quantity) {
success
message
cart {
id
totalItems
totalPrice
items {
id
product {
id
name
price
}
quantity
}
}
}
}
`;
const GET_CART = gql`
query GetCart {
cart {
id
totalItems
totalPrice
}
}
`;
interface AddToCartButtonProps {
productId: string;
disabled?: boolean;
}
const AddToCartButton: React.FC<AddToCartButtonProps> = ({
productId,
disabled
}) => {
const [quantity, setQuantity] = useState(1);
const [addToCart, { loading, error }] = useMutation(ADD_TO_CART, {
variables: { productId, quantity },
// Mutation sonrası cache'i güncelle
update(cache, { data: { addToCart } }) {
if (addToCart.success) {
// Mevcut cart query'sini cache'de güncelle
cache.writeQuery({
query: GET_CART,
data: { cart: addToCart.cart },
});
}
},
onCompleted(data) {
if (data.addToCart.success) {
alert('Ürün sepete eklendi!');
} else {
alert(data.addToCart.message);
}
},
onError(err) {
console.error('Sepete ekleme hatası:', err);
// Sentry veya benzeri bir servise gönderebilirsiniz
},
});
return (
<div className="add-to-cart">
<input
type="number"
min="1"
max="99"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value))}
/>
<button
onClick={() => addToCart()}
disabled={loading || disabled}
>
{loading ? 'Ekleniyor...' : 'Sepete Ekle'}
</button>
{error && (
<p className="error">Bir hata oluştu, lütfen tekrar deneyin.</p>
)}
</div>
);
};
export default AddToCartButton;
update fonksiyonu burada kritik. Mutation tamamlandığında Apollo’ya “bu cache entry’leri de güncelle” diyoruz. Bunu yapmazsanız, kullanıcı sepet sayfasına gittiğinde eski veriyi görür.
Optimistic Updates ile Anlık Geri Bildirim
Kullanıcı bir butona bastığında sonucu beklemeden arayüzü güncellemek istiyorsanız optimistic updates kullanın. Özellikle like/beğeni sistemlerinde veya stok sayaçlarında çok işe yarıyor:
// src/components/WishlistButton.tsx
import React from 'react';
import { useMutation, gql, useApolloClient } from '@apollo/client';
const TOGGLE_WISHLIST = gql`
mutation ToggleWishlist($productId: ID!) {
toggleWishlist(productId: $productId) {
id
isInWishlist
wishlistCount
}
}
`;
const WishlistButton: React.FC<{
productId: string;
isInWishlist: boolean;
wishlistCount: number;
}> = ({ productId, isInWishlist, wishlistCount }) => {
const [toggleWishlist] = useMutation(TOGGLE_WISHLIST, {
variables: { productId },
optimisticResponse: {
toggleWishlist: {
__typename: 'Product',
id: productId,
isInWishlist: !isInWishlist, // Anlık toggle
wishlistCount: isInWishlist
? wishlistCount - 1
: wishlistCount + 1,
},
},
});
return (
<button
onClick={() => toggleWishlist()}
className={isInWishlist ? 'wishlisted' : 'not-wishlisted'}
>
{isInWishlist ? '❤️' : '🤍'} {wishlistCount}
</button>
);
};
export default WishlistButton;
optimisticResponse ile UI anında güncelleniyor. Eğer sunucu isteği başarısız olursa Apollo otomatik olarak cache’i eski haline döndürüyor. Bu rollback mekanizması kutudan çıkar çıkmaz geliyor, kendiniz yazmak zorunda değilsiniz.
Subscription ile Gerçek Zamanlı Veri
WebSocket üzerinden gerçek zamanlı veri akışı için subscription’ları kullanıyoruz. Önce WebSocket link’i ekleyelim:
// src/apollo/client.ts - WebSocket destekli versiyon
import {
ApolloClient,
InMemoryCache,
createHttpLink,
split,
} from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: process.env.REACT_APP_GRAPHQL_URL || 'http://localhost:4000/graphql',
});
const wsLink = new GraphQLWsLink(
createClient({
url: process.env.REACT_APP_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql',
connectionParams: () => {
const token = localStorage.getItem('authToken');
return { authorization: token ? `Bearer ${token}` : '' };
},
retryAttempts: 5, // Bağlantı koptuğunda 5 kez tekrar dene
})
);
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Subscription ise WebSocket, değilse HTTP kullan
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
authLink.concat(httpLink)
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Şimdi gerçek zamanlı stok takibi için bir subscription kullanalım:
// src/components/StockTracker.tsx
import React from 'react';
import { useSubscription, gql } from '@apollo/client';
const STOCK_UPDATED = gql`
subscription OnStockUpdated($productId: ID!) {
stockUpdated(productId: $productId) {
productId
stock
updatedAt
}
}
`;
const StockTracker: React.FC<{ productId: string; initialStock: number }> = ({
productId,
initialStock,
}) => {
const { data, loading } = useSubscription(STOCK_UPDATED, {
variables: { productId },
onData({ data: { data } }) {
if (data?.stockUpdated.stock === 0) {
// Stok tükendi bildirimi
console.warn(`Ürün ${productId} stoku tükendi!`);
}
},
});
const currentStock = data?.stockUpdated.stock ?? initialStock;
return (
<div className="stock-tracker">
<span className={currentStock > 0 ? 'available' : 'unavailable'}>
{loading ? 'Bağlanıyor...' : `Stok: ${currentStock}`}
</span>
{currentStock > 0 && currentStock <= 5 && (
<span className="warning">Son {currentStock} ürün!</span>
)}
</div>
);
};
export default StockTracker;
WebSocket bağlantısında dikkat etmeniz gereken bir nokta: production’da load balancer kullanıyorsanız sticky session’ları etkinleştirmeniz gerekiyor. Aksi hâlde WebSocket bağlantısı sürekli kopup yeniden bağlanmaya çalışır.
Error Handling ve Apollo Error Policies
Üretim ortamında error handling’i ciddiye almak gerekiyor. Apollo’nun error objesi aslında iki farklı hata tipini barındırıyor: network hataları ve GraphQL hataları. Bunları birbirine karıştırmak sık yapılan bir hata.
// src/hooks/useProductDetail.ts
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCT_DETAIL = gql`
query GetProductDetail($id: ID!) {
product(id: $id) {
id
name
description
price
stock
reviews {
id
rating
comment
user {
id
name
}
}
}
}
`;
export const useProductDetail = (productId: string) => {
const { loading, error, data, networkStatus } = useQuery(GET_PRODUCT_DETAIL, {
variables: { id: productId },
errorPolicy: 'partial', // Kısmi veri gelirse de göster
notifyOnNetworkStatusChange: true,
skip: !productId, // productId yoksa query'yi çalıştırma
});
// Network hatası (sunucu erişilemiyor)
const isNetworkError = error?.networkError !== null;
// GraphQL hatası (validation, authorization vb.)
const graphqlErrors = error?.graphQLErrors || [];
const isAuthError = graphqlErrors.some(
(err) => err.extensions?.code === 'UNAUTHENTICATED'
);
return {
product: data?.product,
loading,
networkStatus,
isNetworkError,
isAuthError,
graphqlErrors,
};
};
errorPolicy: 'partial' kullandığınızda, örneğin reviews bölümünde bir hata olsa bile product’ın diğer alanları geliyorsa Apollo bunları size veriyor. Çoğu durumda kullanıcıya kısmi veri göstermek hiç veri göstermemekten daha iyi.
Fragment Kullanımı ve Kod Organizasyonu
Büyük projelerde aynı alanları birden fazla query’de tekrar yazmak kaçınılmaz hâle geliyor. Fragment’lar bu sorunu çözüyor:
// src/graphql/fragments.ts
import { gql } from '@apollo/client';
export const PRODUCT_BASE_FIELDS = gql`
fragment ProductBaseFields on Product {
id
sku
name
price
stock
images {
url
alt
}
}
`;
export const PRODUCT_DETAIL_FIELDS = gql`
fragment ProductDetailFields on Product {
...ProductBaseFields
description
specifications {
key
value
}
category {
id
name
slug
}
}
${PRODUCT_BASE_FIELDS}
`;
// src/components/FeaturedProducts.tsx
import { useQuery, gql } from '@apollo/client';
import { PRODUCT_BASE_FIELDS } from '../graphql/fragments';
const GET_FEATURED = gql`
query GetFeatured {
featuredProducts {
...ProductBaseFields
}
}
${PRODUCT_BASE_FIELDS}
`;
Fragment’ları ayrı bir dosyada tutmak ve import ederek kullanmak, query’lerdeki tekrarı ciddi ölçüde azaltıyor. Özellikle 20-30 farklı query olan projelerde bu organizasyonun değeri ortaya çıkıyor.
Apollo DevTools ile Debug
Geliştirme sürecinde Chrome için Apollo Client DevTools eklentisini mutlaka kurun:
# .env.development dosyanıza ekleyin
REACT_APP_GRAPHQL_URL=http://localhost:4000/graphql
REACT_APP_GRAPHQL_WS_URL=ws://localhost:4000/graphql
Client’ınızı development için ayrı yapılandırabilirsiniz:
// src/apollo/client.ts içinde
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
connectToDevTools: process.env.NODE_ENV === 'development',
// Development'ta cache inspection için
...(process.env.NODE_ENV === 'development' && {
defaultOptions: {
watchQuery: {
fetchPolicy: 'network-only', // Cache sorunlarını maskeleme
},
},
}),
});
DevTools ile hangi query’lerin çalıştığını, cache’de ne olduğunu ve mutation geçmişini görebiliyorsunuz. Özellikle “neden bu component re-render olmadı?” ya da “cache neden güncellenmiyor?” sorularının cevabını bulmak çok kolaylaşıyor.
Performans Optimizasyonu: useLazyQuery ve Pagination
Her zaman component mount olduğunda query çalıştırmak istemeyebilirsiniz. Kullanıcı bir butona bastığında ya da belirli bir event gerçekleştiğinde çalıştırmak için useLazyQuery kullanın. Bunun yanında büyük veri setlerinde pagination kaçınılmaz:
// src/components/ProductSearch.tsx
import React, { useState } from 'react';
import { useLazyQuery, gql } from '@apollo/client';
const SEARCH_PRODUCTS = gql`
query SearchProducts($query: String!, $page: Int!, $pageSize: Int!) {
searchProducts(query: $query, page: $page, pageSize: $pageSize) {
items {
id
name
price
images { url alt }
}
pagination {
currentPage
totalPages
totalItems
hasNextPage
}
}
}
`;
const ProductSearch: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [searchProducts, { loading, data, fetchMore }] = useLazyQuery(
SEARCH_PRODUCTS,
{
fetchPolicy: 'cache-and-network',
}
);
const handleSearch = () => {
if (searchQuery.trim()) {
setCurrentPage(1);
searchProducts({
variables: { query: searchQuery, page: 1, pageSize: 12 },
});
}
};
const handleLoadMore = () => {
const nextPage = currentPage + 1;
fetchMore({
variables: {
query: searchQuery,
page: nextPage,
pageSize: 12,
},
updateQuery(previousResult, { fetchMoreResult }) {
if (!fetchMoreResult) return previousResult;
return {
searchProducts: {
...fetchMoreResult.searchProducts,
items: [
...previousResult.searchProducts.items,
...fetchMoreResult.searchProducts.items,
],
},
};
},
});
setCurrentPage(nextPage);
};
const results = data?.searchProducts;
return (
<div>
<div className="search-bar">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Ürün ara..."
/>
<button onClick={handleSearch} disabled={loading}>
{loading ? 'Aranıyor...' : 'Ara'}
</button>
</div>
{results && (
<>
<p>{results.pagination.totalItems} ürün bulundu</p>
<div className="results-grid">
{results.items.map((item: any) => (
<div key={item.id}>{item.name} - {item.price} TL</div>
))}
</div>
{results.pagination.hasNextPage && (
<button onClick={handleLoadMore} disabled={loading}>
Daha Fazla Göster
</button>
)}
</>
)}
</div>
);
};
export default ProductSearch;
fetchMore ile infinite scroll ya da “daha fazla göster” yapısı kurarken updateQuery fonksiyonunda önceki sonuçlarla yeni sonuçları birleştirmeyi unutmayın. Bunu yapmazsanız her sayfada önceki sonuçlar kayboluyor.
Sonuç
Apollo Client ile React’i entegre etmek başta biraz öğrenme eğrisi gerektiriyor, özellikle cache mekanizmasını kavramak zaman alıyor. Ama bir kez oturduktan sonra, her şeyi manuel yazmaktan ne kadar zaman kazandırdığını açıkça görüyorsunuz.
Dikkat etmeniz gereken birkaç kritik nokta var:
- Cache’i doğru yapılandırın:
typePoliciesvekeyFieldsayarlarını ihmal etmeyin. Yoksa normalize edilmemiş cache size production’da baş ağrıtır. - fetchPolicy’i duruma göre seçin: Her query için aynı politika uygulamayın. Sık değişen veriler için
cache-and-network, nadiren değişenler içincache-firsttercih edin. - Mutation sonrası cache güncellemesini unutmayın:
updatecallback’i ya darefetchQuerieskullanarak ilgili query’leri güncelleyin. - Error handling’i katmanlı düşünün: Network hatalarını ve GraphQL hatalarını farklı şekillerde ele alın.
- Fragment’ları erken benimseyin: Proje büyüdükçe fragment olmadan query’leri yönetmek zorlaşıyor.
WebSocket subscription’larını production’a taşırken load balancer sticky session ayarlarına dikkat edin. Bunu atlayan birkaç ekip gördüm, saatlerce “bağlantı neden kesiliyor?” diye uğraştılar.
Apollo ekosistemi aktif olarak gelişiyor, Apollo Client 4 sürümü de geliyor. Mevcut API’lerin büyük kısmı korunuyor ama bazı breaking change’ler olacak. Release note’ları takip etmenizi öneririm.
