Dynamic frontend integration
Integrate Optimizely Graph with modern frontend frameworks like React, Vue, Angular, and Next.js for performant, reusable component-based architectures.
Dynamic frontend integration connects Optimizely Graph to modern JavaScript frameworks, enabling you to bind GraphQL data to UI components in a performant, reusable way.
This is the HOW-TO guide: complete code examples for integrating Graph with React, Vue, Angular, Next.js, iOS, Android, and React Native. For architectural decisions (WHEN to use SSG vs SSR vs CSR), see Headless architecture patterns.
What you'll find here:
- ✅ Framework setup and configuration
- ✅ Complete working code examples
- ✅ Apollo Client, React Query, and native fetch implementations
- ✅ Error handling, loading states, caching
- ✅ Mobile platform integration (iOS, Android, React Native)
What you WON'T find here:
- ❌ Architectural decision-making (see Headless architecture patterns)
- ❌ Platform-specific query optimization (see Omnichannel delivery)
- ❌ Multi-service integration patterns (see Composable architecture)
Framework selection
Choose based on your team's expertise and project requirements:
| Framework | Best for | Graph integration |
|---|---|---|
| React | Component-based UIs, large ecosystems | Apollo Client, React Query |
| Next.js | React with SSR/SSG, production sites | Native fetch, Apollo Client |
| Vue | Progressive adoption, simpler learning curve | Apollo Client, Vue Query |
| Angular | Enterprise apps, TypeScript-first | Apollo Angular |
| Svelte | Minimal bundle size, reactive by default | GraphQL Request, Svelte Query |
React integration
Setup with Apollo Client
Apollo Client is the most popular GraphQL client for React:
npm install @apollo/client graphqlConfiguration:
Security note
Only use
NEXT_PUBLIC_*environment variables when you are using a public single key intended for read-only public content. For bearer/HMAC/admin credentials, keep secrets server-side (API routes/BFF) so they are never shipped to the browser.
// lib/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_GRAPH_ENDPOINT,
headers: {
Authorization: `epi-single ${process.env.NEXT_PUBLIC_GRAPH_SINGLE_KEY}`
}
});
export const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
Content: {
// Customize caching behavior
keyArgs: ['where', 'orderBy'],
merge(existing = { items: [] }, incoming) {
return {
...incoming,
items: [...existing.items, ...incoming.items]
};
}
}
}
}
}
})
});App wrapper:
// pages/_app.js
import { ApolloProvider } from '@apollo/client';
import { client } from '../lib/apollo-client';
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;Query data in components
// components/ArticleList.js
import { useQuery, gql } from '@apollo/client';
const GET_ARTICLES = gql`
query GetArticles($limit: Int!, $skip: Int, $category: String) {
Article(
where: { category: { eq: $category } }
orderBy: { publishedDate: DESC }
limit: $limit
skip: $skip
) {
items {
id
title
excerpt
publishedDate
author {
name
avatar
}
image {
url
}
}
total
}
}
`;
function ArticleList({ category, limit = 10 }) {
const { data, loading, error, fetchMore } = useQuery(GET_ARTICLES, {
variables: { category, limit, skip: 0 }
});
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
const articles = data.Article.items;
return (
<div className="article-list">
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
{articles.length < data.Article.total && (
<button onClick={() => fetchMore({
variables: {
limit: 10,
skip: articles.length // Skip what is already loaded
}
})}>
Load More
</button>
)}
</div>
);
}Using React Query (alternative)
React Query offers a simpler API for GraphQL:
npm install @tanstack/react-query// hooks/useGraphQuery.js
import { useQuery } from '@tanstack/react-query';
export function useGraphQuery(query, variables) {
return useQuery({
queryKey: [query, variables],
queryFn: async () => {
const response = await fetch(process.env.NEXT_PUBLIC_GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.NEXT_PUBLIC_GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({ query, variables })
});
const { data, errors } = await response.json();
if (errors) throw new Error(errors[0].message);
return data;
},
staleTime: 60000, // Consider data fresh for 1 minute
cacheTime: 300000 // Keep in cache for 5 minutes
});
}Usage:
function ProductList({ category }) {
const { data, isLoading, error } = useGraphQuery(
`query GetProducts($category: String!) {
Product(where: { category: { eq: $category } }) {
items { id, name, price, image { url } }
}
}`,
{ category }
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="product-grid">
{data.Product.items.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Next.js integration
Next.js combines SSG, SSR, and CSR capabilities. For architectural guidance on when to use each pattern, see Headless architecture patterns. This section shows how to implement each pattern.
Pattern 1: Static site generation (SSG)
Use getStaticProps() to fetch content at build time and getStaticPaths() to generate dynamic routes.
A single unpaged query returns only the first page of results, so a site with more articles than the default page size silently loses routes. Page through the full set with skip and limit, advancing skip by the batch size until a batch comes back shorter than requested.
Skip and limit reach only the first 10,000 hits, and Graph enforces that ceiling by rejecting the request – it does not return a truncated result. Both skip and skip + limit are validated, so the loop must stop before skip + limit exceeds 10,000 rather than relying on the short-batch check to end it.
// pages/blog/[slug].js
const SLUG_BATCH_SIZE = 100;
// Graph rejects a request when skip exceeds 10,000 or when skip + limit does,
// so the loop stops at the ceiling instead of requesting past it
const MAX_SKIP_LIMIT = 10000;
async function fetchAllArticleSlugs() {
const slugs = [];
let skip = 0;
let exhausted = false;
while (skip + SLUG_BATCH_SIZE <= MAX_SKIP_LIMIT) {
const response = await fetch(process.env.GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({
query: `
query GetArticleSlugs($skip: Int!, $limit: Int!) {
Article(
skip: $skip
limit: $limit
# Stable ordering so items do not shift between batches
orderBy: { publishedDate: DESC }
) {
items { slug }
total
}
}
`,
variables: { skip, limit: SLUG_BATCH_SIZE }
})
});
// An out-of-range skip or limit fails the request itself, so check the status
if (!response.ok) {
throw new Error(`Graph returned HTTP ${response.status} for skip ${skip}`);
}
const { data, errors } = await response.json();
if (errors) throw new Error(errors[0].message);
const batch = data.Article;
slugs.push(...batch.items.map(article => article.slug));
// A short batch means the result set is exhausted
if (batch.items.length < SLUG_BATCH_SIZE) {
exhausted = true;
break;
}
skip += SLUG_BATCH_SIZE;
}
// Reaching the ceiling with full batches means articles remain out of reach
if (!exhausted) {
throw new Error(
`More than ${MAX_SKIP_LIMIT} articles: skip and limit cannot enumerate the rest. ` +
'Prerender a subset or switch to cursor-based pagination.'
);
}
return slugs;
}
export async function getStaticPaths() {
const slugs = await fetchAllArticleSlugs();
return {
paths: slugs.map(slug => ({ params: { slug } })),
fallback: 'blocking'
};
}
export async function getStaticProps({ params, locale }) {
const response = await fetch(process.env.GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({
query: `
query GetArticle($slug: String!, $locale: [Locales]) {
Article(
where: { slug: { eq: $slug } }
locale: $locale
) {
item {
title
body
publishedDate
author { name }
}
}
}
`,
variables: { slug: params.slug, locale: [locale] }
})
});
const { data } = await response.json();
return {
props: { article: data.Article.item },
revalidate: 60 // ISR: regenerate every 60 seconds
};
}
function ArticlePage({ article }) {
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</article>
);
}Keep the following in mind when paging through paths at build time:
- Skip and limit reach only the first 10,000 hits, and requests past that boundary are rejected rather than served slowly. Graph validates
skipandskip + limitseparately, so a batch size of 100 fails atskip: 9950– that request is within theskipmaximum but itsskip + limitof 10,050 is not. Guard the loop against the ceiling, as shown above, and either prerender a subset or switch to cursor-based pagination for sites with more articles than that. - Always pair
skipwith a stableorderBy. Without a tiebreaker such as_id, items that share a sort value can shift between batches, which duplicates some slugs and drops others. - Project only the fields the paths need. Fetching
slugalone keeps each batch small and the build fast. - For very large sites, skip the full enumeration and prerender only the pages that matter most – for example the most recently published articles – letting
fallback: 'blocking'render the remainder on first request:
export async function getStaticPaths() {
// Prerender the 500 newest articles; generate the rest on demand
const response = await fetch(process.env.GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({
query: `
query GetRecentArticleSlugs {
Article(limit: 500, orderBy: { publishedDate: DESC }) {
items { slug }
}
}
`
})
});
const { data } = await response.json();
return {
paths: data.Article.items.map(article => ({
params: { slug: article.slug }
})),
fallback: 'blocking'
};
}With Incremental Static Regeneration (ISR):
// pages/blog/[slug].js
export async function getStaticProps({ params, locale }) {
const response = await fetch(process.env.GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({
query: `
query GetArticle($slug: String!, $locale: [Locales]) {
Article(
where: { slug: { eq: $slug } }
locale: $locale
) {
item {
title
body
publishedDate
author { name, avatar }
}
}
}
`,
variables: { slug: params.slug, locale: [locale] }
})
});
const { data } = await response.json();
if (!data?.Article?.item) {
return { notFound: true };
}
return {
props: { article: data.Article.item },
revalidate: 60 // Regenerate page every 60 seconds (ISR)
};
}
function ArticlePage({ article }) {
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</article>
);
}
export default ArticlePage;Pattern 2: Server-side rendering (SSR)
Use getServerSideProps() to fetch content on every request.
// pages/products/[key].js
export async function getServerSideProps({ params, req, locale }) {
// Query Graph on each request for fresh data
const response = await fetch(process.env.GRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
},
body: JSON.stringify({
query: `
query GetProduct($key: String!, $locale: [Locales]) {
Product(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
name
price
inventory
description
images { url }
}
}
}
`,
variables: { key: params.key, locale: [locale] }
})
});
const { data } = await response.json();
if (!data?.Product?.item) {
return { notFound: true };
}
return {
props: {
product: data.Product.item,
timestamp: new Date().toISOString() // Shows this was rendered fresh
}
};
}
function ProductPage({ product, timestamp }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<p>In Stock: {product.inventory}</p>
<small>Rendered at: {timestamp}</small>
</div>
);
}
export default ProductPage;Pattern 3: Client-side rendering (CSR)
Fetch data in the browser using React hooks or Apollo Client. See the React integration section for examples.
Vue integration
Setup with Apollo Client
npm install @vue/apollo-composable @apollo/client graphqlConfiguration:
// plugins/apollo.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core';
import { DefaultApolloClient } from '@vue/apollo-composable';
const httpLink = new HttpLink({
uri: import.meta.env.VITE_GRAPH_ENDPOINT,
headers: {
Authorization: `epi-single ${import.meta.env.VITE_GRAPH_SINGLE_KEY}`
}
});
const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
});
export default {
install(app) {
app.provide(DefaultApolloClient, apolloClient);
}
};Component usage:
<!-- components/ArticleList.vue -->
<template>
<div class="article-list">
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<article-card
v-for="article in articles"
:key="article.id"
:article="article"
/>
</div>
</div>
</template>
<script setup>
import { useQuery } from '@vue/apollo-composable';
import { gql } from '@apollo/client/core';
const props = defineProps({
category: String,
limit: { type: Number, default: 10 }
});
const GET_ARTICLES = gql`
query GetArticles($category: String, $limit: Int!) {
Article(
where: { category: { eq: $category } }
limit: $limit
) {
items {
id
title
excerpt
image { url }
}
}
}
`;
const { result, loading, error } = useQuery(GET_ARTICLES, {
category: props.category,
limit: props.limit
});
const articles = computed(() => result.value?.Article?.items ?? []);
</script>Nuxt 3 integration
// composables/useGraph.js
export const useGraph = () => {
const config = useRuntimeConfig();
const query = async (graphqlQuery, variables = {}) => {
const { data, error } = await useFetch(config.public.graphEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `epi-single ${config.public.graphSingleKey}`
},
body: {
query: graphqlQuery,
variables
}
});
if (error.value) throw error.value;
return data.value.data;
};
return { query };
};Usage in Nuxt component:
<script setup>
const { query } = useGraph();
const { data: articles } = await useAsyncData('articles', () =>
query(`
query {
Article(limit: 10) {
items { id, title, excerpt }
}
}
`)
);
</script>
<template>
<div>
<article-card
v-for="article in articles.Article.items"
:key="article.id"
:article="article"
/>
</div>
</template>Native mobile integration
iOS (Swift)
Integrate Graph with native iOS applications:
// GraphQLClient.swift
import Foundation
struct GraphQLClient {
private let endpoint = URL(string: "https://cg.optimizely.com/content/v2")!
private let singleKey: String
init(singleKey: String) {
self.singleKey = singleKey
}
func fetchProducts(limit: Int) async throws -> [Product] {
let query = """
query GetProducts($limit: Int!) {
Product(limit: $limit, orderBy: { featured: DESC }) {
items {
id
name
price
image {
mobileUrl
}
}
}
}
"""
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("epi-single \(singleKey)", forHTTPHeaderField: "Authorization")
let body = ["query": query, "variables": ["limit": limit]]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(GraphQLResponse.self, from: data)
return response.data.Product.items
}
}
// Models
struct GraphQLResponse: Codable {
let data: ProductData
}
struct ProductData: Codable {
let Product: ProductList
}
struct ProductList: Codable {
let items: [Product]
}
struct Product: Codable, Identifiable {
let id: String
let name: String
let price: Double
let image: ProductImage
}
struct ProductImage: Codable {
let mobileUrl: String
}SwiftUI integration:
// ProductListView.swift
import SwiftUI
struct ProductListView: View {
@StateObject private var viewModel = ProductViewModel()
var body: some View {
List(viewModel.products) { product in
ProductRow(product: product)
}
.task {
await viewModel.loadProducts()
}
}
}
class ProductViewModel: ObservableObject {
@Published var products: [Product] = []
private let client = GraphQLClient(singleKey: "YOUR_SINGLE_KEY")
func loadProducts() async {
do {
products = try await client.fetchProducts(limit: 20)
} catch {
print("Failed to load products: \(error)")
}
}
}Android (Kotlin)
Integrate Graph with native Android applications:
// GraphQLClient.kt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.MediaType.Companion.toMediaType
import org.json.JSONObject
class GraphQLClient(private val singleKey: String) {
private val client = OkHttpClient()
private val endpoint = "https://cg.optimizely.com/content/v2"
suspend fun fetchProducts(limit: Int): List<Product> = withContext(Dispatchers.IO) {
val query = """
query GetProducts(${'$'}limit: Int!) {
Product(limit: ${'$'}limit, orderBy: { featured: DESC }) {
items {
id
name
price
image {
mobileUrl
}
}
}
}
"""
val json = JSONObject().apply {
put("query", query)
put("variables", JSONObject().put("limit", limit))
}
val request = Request.Builder()
.url(endpoint)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "epi-single $singleKey")
.post(json.toString().toRequestBody("application/json".toMediaType()))
.build()
val response = client.newCall(request).execute()
val responseBody = response.body?.string() ?: throw Exception("Empty response")
parseProducts(responseBody)
}
private fun parseProducts(json: String): List<Product> {
val jsonObject = JSONObject(json)
val items = jsonObject.getJSONObject("data")
.getJSONObject("Product")
.getJSONArray("items")
return (0 until items.length()).map { index ->
val item = items.getJSONObject(index)
Product(
id = item.getString("id"),
name = item.getString("name"),
price = item.getDouble("price"),
imageUrl = item.getJSONObject("image").getString("mobileUrl")
)
}
}
}
// Models
data class Product(
val id: String,
val name: String,
val price: Double,
val imageUrl: String
)Jetpack Compose integration:
// ProductListScreen.kt
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
@Composable
fun ProductListScreen(viewModel: ProductViewModel = viewModel()) {
val products by viewModel.products.collectAsState()
LazyColumn {
items(products) { product ->
ProductCard(product)
}
}
LaunchedEffect(Unit) {
viewModel.loadProducts()
}
}
class ProductViewModel : ViewModel() {
private val client = GraphQLClient(singleKey = "YOUR_SINGLE_KEY")
private val _products = MutableStateFlow<List<Product>>(emptyList())
val products: StateFlow<List<Product>> = _products
fun loadProducts() {
viewModelScope.launch {
try {
_products.value = client.fetchProducts(limit = 20)
} catch (e: Exception) {
// Handle error
}
}
}
}React Native
For cross-platform mobile development:
// hooks/useGraphQuery.js
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCTS = gql`
query GetProducts($limit: Int!, $skip: Int) {
Product(
limit: $limit
skip: $skip
orderBy: { featured: DESC }
) {
items {
id
name
price
image {
mobileUrl
}
}
total
}
}
`;
export function useProducts(limit = 20) {
return useQuery(GET_PRODUCTS, {
variables: { limit, skip: 0 },
fetchPolicy: 'cache-and-network', // Mobile-optimized caching
});
}Component usage:
// screens/ProductListScreen.js
import React from 'react';
import { FlatList, ActivityIndicator } from 'react-native';
import { useProducts } from '../hooks/useGraphQuery';
import ProductCard from '../components/ProductCard';
export default function ProductListScreen() {
const { data, loading, fetchMore } = useProducts(20);
const loadMore = () => {
const loaded = data?.Product?.items?.length ?? 0;
// Stop requesting once every product is loaded
if (loaded < (data?.Product?.total ?? 0)) {
fetchMore({
variables: { skip: loaded }
});
}
};
if (loading && !data) {
return <ActivityIndicator size="large" />;
}
return (
<FlatList
data={data?.Product?.items || []}
renderItem={({ item }) => <ProductCard product={item} />}
keyExtractor={item => item.id}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
);
}Offline caching:
// apollo-client.js
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { persistCache } from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
const cache = new InMemoryCache();
// Persist cache for offline support
await persistCache({
cache,
storage: AsyncStorage,
maxSize: 10485760, // 10 MB
});
const client = new ApolloClient({
uri: 'https://cg.optimizely.com/content/v2',
cache,
headers: {
Authorization: `epi-single ${process.env.GRAPH_SINGLE_KEY}`
}
});Angular integration
Setup with Apollo Angular
npm install apollo-angular @apollo/client graphqlConfiguration:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
{
provide: APOLLO_OPTIONS,
useFactory(httpLink: HttpLink) {
return {
cache: new InMemoryCache(),
link: httpLink.create({
uri: 'https://cg.optimizely.com/content/v2',
headers: {
Authorization: `epi-single ${environment.graphSingleKey}`
}
})
};
},
deps: [HttpLink]
}
]
};Service:
// services/article.service.ts
import { Injectable } from '@angular/core';
import { Apollo, gql } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
const GET_ARTICLES = gql`
query GetArticles($limit: Int!) {
Article(limit: $limit) {
items {
id
title
excerpt
image { url }
}
}
}
`;
@Injectable({ providedIn: 'root' })
export class ArticleService {
constructor(private apollo: Apollo) {}
getArticles(limit: number = 10): Observable<Article[]> {
return this.apollo
.watchQuery({
query: GET_ARTICLES,
variables: { limit }
})
.valueChanges
.pipe(
map(result => result.data.Article.items)
);
}
}Component:
// components/article-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ArticleService } from '../services/article.service';
@Component({
selector: 'app-article-list',
template: `
<div class="article-list">
<app-article-card
*ngFor="let article of articles$ | async"
[article]="article">
</app-article-card>
</div>
`
})
export class ArticleListComponent implements OnInit {
articles$: Observable<Article[]>;
constructor(private articleService: ArticleService) {}
ngOnInit() {
this.articles$ = this.articleService.getArticles();
}
}Best practices
1. Fragment reuse
Define reusable fragments for common field sets:
// fragments/article.js
import { gql } from '@apollo/client';
export const ARTICLE_CARD_FRAGMENT = gql`
fragment ArticleCardFields on Article {
id
title
excerpt
publishedDate
author {
name
avatar
}
image {
url
}
}
`;
// Usage
const GET_ARTICLES = gql`
${ARTICLE_CARD_FRAGMENT}
query GetArticles($limit: Int!) {
Article(limit: $limit) {
items {
...ArticleCardFields
}
}
}
`;2. Loading and error states
Always handle loading and error states:
function ContentComponent() {
const { data, loading, error } = useQuery(GET_CONTENT);
if (loading) {
return <Skeleton count={5} />;
}
if (error) {
return (
<ErrorBoundary>
<ErrorMessage
message="Failed to load content"
retry={() => refetch()}
/>
</ErrorBoundary>
);
}
return <Content data={data} />;
}3. Type safety with TypeScript
Generate types from your Graph schema:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript# codegen.yml
schema: https://cg.optimizely.com/content/v2
documents: ./src/**/*.graphql
generates:
./src/types/graph.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo4. Performance monitoring
Track query performance:
import { ApolloLink } from '@apollo/client';
const performanceLink = new ApolloLink((operation, forward) => {
const startTime = performance.now();
return forward(operation).map(response => {
const endTime = performance.now();
console.log(`Query ${operation.operationName} took ${endTime - startTime}ms`);
analytics.track('graphql_query', {
operation: operation.operationName,
duration: endTime - startTime
});
return response;
});
});Platform-specific considerations
Mobile (iOS, Android, React Native)
For mobile platform-specific query strategies and optimization, see Omnichannel content delivery.
Key optimizations:
- Select the mobile image URL field (for example,
mobileUrl) instead of the desktop rendition - Request summaries instead of full content
- Implement aggressive caching for offline support
- Use pagination/infinite scroll
- Optimize for cellular networks
Kiosks and IoT
For kiosk and IoT device integration patterns, see Omnichannel content delivery.
Next steps
- Review Headless architecture patterns for SSG/SSR/CSR architectural guidance
- Learn Caching best practices to optimize performance
- Explore Composable architecture for multi-service system design
- See Omnichannel content delivery for platform-specific strategies
Updated 1 day ago
