GraphQL Client Libraries - A Comprehensive Guide
GraphQL Client Libraries: A Comprehensive Guide
GraphQL has revolutionized how we think about API design and data fetching in modern web applications. While the GraphQL specification defines the query language and execution semantics, client libraries provide the tools and abstractions needed to seamlessly integrate GraphQL into frontend applications. This guide explores the most popular GraphQL client libraries, with a focus on implementation in React and Next.js projects.
Why Use a GraphQL Client Library?
While you can use GraphQL with plain HTTP requests, dedicated client libraries offer significant advantages:
- Declarative Data Fetching: Define what data you need right alongside your UI components
- Caching: Intelligent client-side caching to avoid redundant network requests
- State Management: Manage remote and local data in a unified way
- Optimistic Updates: Update the UI immediately before server confirmation
- Error Handling: Standardized error handling patterns
- Type Safety: Integration with TypeScript for type-safe queries
- Pagination: Simplified handling of paginated data
- Real-time Updates: Support for subscriptions and live data
Popular GraphQL Client Libraries
1. Apollo Client
Apollo Client is the most comprehensive and widely-used GraphQL client library, offering a robust feature set and extensive ecosystem.
Key Features
- Complete GraphQL client solution with intelligent caching
- Optimistic UI updates
- Error handling and retry logic
- Local state management
- Subscription support
- Extensive plugin ecosystem
- Strong TypeScript integration
- DevTools for debugging
Basic Setup with React
npm install @apollo/client graphql
// src/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: 'https://your-graphql-endpoint.com/graphql',
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
export default client;
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/client';
import client from './apollo-client';
import App from './App';
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
Basic Query Example
import { useQuery, gql } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`;
function PostList() {
const { loading, error, data } = useQuery(GET_POSTS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author.name}</p>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
Mutation Example
import { useMutation, gql } from '@apollo/client';
const ADD_POST = gql`
mutation AddPost($title: String!, $body: String!, $authorId: ID!) {
addPost(title: $title, body: $body, authorId: $authorId) {
id
title
body
author {
id
name
}
}
}
`;
function AddPostForm() {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [authorId, setAuthorId] = useState('');
const [addPost, { loading, error }] = useMutation(ADD_POST, {
update(cache, { data: { addPost } }) {
// Update the cache to include the new post
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: addPost,
fragment: gql`
fragment NewPost on Post {
id
title
body
author {
id
name
}
}
`
});
return [...existingPosts, newPostRef];
}
}
});
}
});
const handleSubmit = (e) => {
e.preventDefault();
addPost({ variables: { title, body, authorId } });
setTitle('');
setBody('');
setAuthorId('');
};
return (
<div>
<h2>Add New Post</h2>
{error && <p>Error: {error.message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="body">Body:</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="authorId">Author ID:</label>
<input
id="authorId"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Post'}
</button>
</form>
</div>
);
}
2. Relay
Relay is Facebook's GraphQL client, designed for building large-scale applications with a focus on performance and scalability.
Key Features
- Highly optimized for performance
- Colocation of components and their data requirements
- Powerful data masking and composition
- Ahead-of-time compilation of queries
- Incremental data fetching
- Strong typing with Flow or TypeScript
- Designed for large-scale applications
Basic Setup with React
npm install react-relay relay-runtime
npm install --save-dev relay-compiler babel-plugin-relay
Add Relay to your Babel configuration:
// .babelrc
{
"plugins": ["relay"]
}
Set up the Relay environment:
// src/relay-environment.js
import {
Environment,
Network,
RecordSource,
Store,
} from 'relay-runtime';
function fetchQuery(operation, variables) {
return fetch('https://your-graphql-endpoint.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: operation.text,
variables,
}),
}).then(response => response.json());
}
const environment = new Environment({
network: Network.create(fetchQuery),
store: new Store(new RecordSource()),
});
export default environment;
Provide the environment to your app:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RelayEnvironmentProvider } from 'react-relay';
import environment from './relay-environment';
import App from './App';
ReactDOM.render(
<RelayEnvironmentProvider environment={environment}>
<App />
</RelayEnvironmentProvider>,
document.getElementById('root')
);
Query Example with Relay
// PostList.js
import React from 'react';
import { graphql, usePreloadedQuery, useQueryLoader } from 'react-relay';
const PostListQuery = graphql`
query PostListQuery {
posts {
id
title
body
author {
id
name
}
}
}
`;
function PostListContent({ queryRef }) {
const data = usePreloadedQuery(PostListQuery, queryRef);
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author.name}</p>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
function PostList() {
const [queryRef, loadQuery] = useQueryLoader(PostListQuery);
React.useEffect(() => {
loadQuery({});
}, [loadQuery]);
return queryRef ? (
<Suspense fallback={<p>Loading...</p>}>
<PostListContent queryRef={queryRef} />
</Suspense>
) : (
<p>Loading...</p>
);
}
3. URQL
URQL is a lightweight and highly customizable GraphQL client that focuses on simplicity and extensibility.
Key Features
- Lightweight and focused API
- Highly customizable with exchanges (middleware)
- Normalized caching (optional)
- Document caching by default
- Extensible architecture
- Good TypeScript support
- Simple to understand and use
Basic Setup with React
npm install urql graphql
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createClient, Provider } from 'urql';
import App from './App';
const client = createClient({
url: 'https://your-graphql-endpoint.com/graphql',
});
ReactDOM.render(
<Provider value={client}>
<App />
</Provider>,
document.getElementById('root')
);
Query Example with URQL
import React from 'react';
import { useQuery, gql } from 'urql';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`;
function PostList() {
const [result] = useQuery({
query: GET_POSTS,
});
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author.name}</p>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
Mutation Example with URQL
import React, { useState } from 'react';
import { useMutation, gql } from 'urql';
const ADD_POST = gql`
mutation AddPost($title: String!, $body: String!, $authorId: ID!) {
addPost(title: $title, body: $body, authorId: $authorId) {
id
title
body
author {
id
name
}
}
}
`;
function AddPostForm() {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [authorId, setAuthorId] = useState('');
const [result, addPost] = useMutation(ADD_POST);
const handleSubmit = (e) => {
e.preventDefault();
addPost({ title, body, authorId }).then(() => {
setTitle('');
setBody('');
setAuthorId('');
});
};
return (
<div>
<h2>Add New Post</h2>
{result.error && <p>Error: {result.error.message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="body">Body:</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="authorId">Author ID:</label>
<input
id="authorId"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
required
/>
</div>
<button type="submit" disabled={result.fetching}>
{result.fetching ? 'Adding...' : 'Add Post'}
</button>
</form>
</div>
);
}
4. SWR with GraphQL
SWR (Stale-While-Revalidate) is a React Hooks library for data fetching that can be used with GraphQL.
Key Features
- Lightweight and focused on data fetching
- Built-in cache and revalidation
- Automatic refetching on focus/reconnect
- Pagination and infinite loading support
- Fast page navigation
- TypeScript ready
- Not GraphQL-specific but works well with it
Basic Setup with React
npm install swr graphql-request graphql
// src/lib/graphql.js
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient('https://your-graphql-endpoint.com/graphql');
export default client;
Query Example with SWR
import React from 'react';
import useSWR from 'swr';
import { gql } from 'graphql-request';
import client from '../lib/graphql';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`;
const fetcher = query => client.request(query);
function PostList() {
const { data, error } = useSWR(GET_POSTS, fetcher);
if (!data) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author.name}</p>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
Mutation Example with SWR
import React, { useState } from 'react';
import { useSWRConfig } from 'swr';
import { gql } from 'graphql-request';
import client from '../lib/graphql';
const ADD_POST = gql`
mutation AddPost($title: String!, $body: String!, $authorId: ID!) {
addPost(title: $title, body: $body, authorId: $authorId) {
id
title
body
author {
id
name
}
}
}
`;
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`;
function AddPostForm() {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [authorId, setAuthorId] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { mutate } = useSWRConfig();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const variables = { title, body, authorId };
const result = await client.request(ADD_POST, variables);
// Update the cache with the new post
mutate(GET_POSTS, async (data) => {
return {
...data,
posts: [...data.posts, result.addPost]
};
}, false);
setTitle('');
setBody('');
setAuthorId('');
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
return (
<div>
<h2>Add New Post</h2>
{error && <p>Error: {error.message}</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="body">Body:</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="authorId">Author ID:</label>
<input
id="authorId"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Post'}
</button>
</form>
</div>
);
}
GraphQL Clients with Next.js
Next.js has specific considerations for GraphQL client integration due to its server-side rendering (SSR) and static site generation (SSG) capabilities.
Apollo Client with Next.js
Setup
Create a utility to initialize Apollo Client with SSR support:
// lib/apollo-client.js
import { useMemo } from 'react';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
let apolloClient;
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined', // Set to true for SSR
link: new HttpLink({
uri: 'https://your-graphql-endpoint.com/graphql',
}),
cache: new InMemoryCache(),
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client,
// the initial state gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Restore the cache using the data passed from
// getStaticProps/getServerSideProps combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState });
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}
Create a custom _app.js
file to provide the Apollo Client:
// pages/_app.js
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../lib/apollo-client';
function MyApp({ Component, pageProps }) {
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
Using getStaticProps with Apollo
// pages/blog/index.js
import { gql } from '@apollo/client';
import { initializeApollo } from '../../lib/apollo-client';
import PostList from '../../components/PostList';
export default function Blog(props) {
return <PostList />;
}
export async function getStaticProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
},
revalidate: 60, // Revalidate every minute
};
}
URQL with Next.js
Setup
// lib/urql-client.js
import {
createClient,
dedupExchange,
cacheExchange,
fetchExchange,
ssrExchange,
} from 'urql';
let urqlClient;
let ssrCache;
export function initializeUrqlClient(initialState = null) {
// For SSR, create a new client and ssrCache for every request
const isServerSide = typeof window === 'undefined';
if (isServerSide) {
ssrCache = ssrExchange({ initialState });
return createClient({
url: 'https://your-graphql-endpoint.com/graphql',
exchanges: [
dedupExchange,
cacheExchange,
ssrCache,
fetchExchange,
],
});
}
// On the client side, reuse the client instance
if (!urqlClient) {
ssrCache = ssrExchange({ initialState });
urqlClient = createClient({
url: 'https://your-graphql-endpoint.com/graphql',
exchanges: [
dedupExchange,
cacheExchange,
ssrCache,
fetchExchange,
],
});
} else if (initialState) {
// If we have initial state, restore it to the ssrCache
ssrCache.restoreData(initialState);
}
return urqlClient;
}
export function useUrqlClient(initialState) {
return initializeUrqlClient(initialState);
}
export { ssrCache };
Create a custom _app.js
file:
// pages/_app.js
import { Provider } from 'urql';
import { useUrqlClient } from '../lib/urql-client';
function MyApp({ Component, pageProps }) {
const client = useUrqlClient(pageProps.urqlState);
return (
<Provider value={client}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
Using getStaticProps with URQL
// pages/blog/index.js
import { gql } from 'urql';
import { initializeUrqlClient, ssrCache } from '../../lib/urql-client';
import PostList from '../../components/PostList';
export default function Blog(props) {
return <PostList />;
}
export async function getStaticProps() {
const client = initializeUrqlClient();
await client.query(gql`
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
`).toPromise();
return {
props: {
urqlState: ssrCache.extractData(),
},
revalidate: 60, // Revalidate every minute
};
}
Advanced Patterns and Best Practices
1. Type Safety with GraphQL Code Generator
GraphQL Code Generator can automatically generate TypeScript types from your GraphQL schema and operations.
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
Create a configuration file:
# codegen.yml
overwrite: true
schema: "https://your-graphql-endpoint.com/graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
Create a GraphQL file for your operations:
# src/graphql/posts.graphql
query GetPosts {
posts {
id
title
body
author {
id
name
}
}
}
mutation AddPost($title: String!, $body: String!, $authorId: ID!) {
addPost(title: $title, body: $body, authorId: $authorId) {
id
title
body
author {
id
name
}
}
}
Run the code generator:
npx graphql-codegen
Use the generated hooks in your components:
import React from 'react';
import { useGetPostsQuery } from '../generated/graphql';
function PostList() {
const { loading, error, data } = useGetPostsQuery();
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data) return <p>No data</p>;
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author.name}</p>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
2. Fragment Colocation
Fragments allow you to define reusable pieces of queries that can be colocated with the components that use them.
import { gql, useFragment } from '@apollo/client';
// Define a fragment for post data
export const POST_FRAGMENT = gql`
fragment PostDetails on Post {
id
title
body
createdAt
author {
id
name
}
}
`;
// Use the fragment in a query
const GET_POSTS = gql`
query GetPosts {
posts {
...PostDetails
}
}
${POST_FRAGMENT}
`;
// PostItem component using the fragment
function PostItem({ post }) {
return (
<div className="post-item">
<h3>{post.title}</h3>
<p className="meta">By {post.author.name} on {new Date(post.createdAt).toLocaleDateString()}</p>
<p>{post.body}</p>
</div>
);
}
// PostList component using the query
function PostList() {
const { loading, error, data } = useQuery(GET_POSTS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div className="post-list">
<h2>Posts</h2>
{data.posts.map(post => (
<PostItem key={post.id} post={post} />
))}
</div>
);
}
3. Optimistic UI Updates
Optimistic UI updates improve the perceived performance by updating the UI before the server responds.
import { useMutation, gql } from '@apollo/client';
const ADD_COMMENT = gql`
mutation AddComment($postId: ID!, $content: String!) {
addComment(postId: $postId, content: $content) {
id
content
createdAt
author {
id
name
}
}
}
`;
const GET_POST = gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
body
comments {
id
content
createdAt
author {
id
name
}
}
}
}
`;
function CommentForm({ postId, currentUser }) {
const [content, setContent] = useState('');
const [addComment] = useMutation(ADD_COMMENT, {
optimisticResponse: {
addComment: {
__typename: 'Comment',
id: 'temp-id-' + Date.now(),
content,
createdAt: new Date().toISOString(),
author: {
__typename: 'User',
id: currentUser.id,
name: currentUser.name
}
}
},
update: (cache, { data: { addComment } }) => {
// Read the current post data from the cache
const data = cache.readQuery({
query: GET_POST,
variables: { id: postId }
});
// Write the updated post data back to the cache
cache.writeQuery({
query: GET_POST,
variables: { id: postId },
data: {
post: {
...data.post,
comments: [...data.post.comments, addComment]
}
}
});
}
});
const handleSubmit = (e) => {
e.preventDefault();
if (!content.trim()) return;
addComment({
variables: { postId, content }
});
setContent('');
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Write a comment..."
required
/>
<button type="submit">Add Comment</button>
</form>
);
}
4. Pagination
Implementing pagination with GraphQL clients:
import { useQuery, gql } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
excerpt
createdAt
}
postsCount
}
`;
function PaginatedPosts() {
const [page, setPage] = useState(1);
const limit = 10;
const offset = (page - 1) * limit;
const { loading, error, data } = useQuery(GET_POSTS, {
variables: { limit, offset },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const totalPages = Math.ceil(data.postsCount / limit);
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
<small>{new Date(post.createdAt).toLocaleDateString()}</small>
</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
Next
</button>
</div>
</div>
);
}
5. Infinite Scrolling
Implementing infinite scrolling with GraphQL:
import { useQuery, gql } from '@apollo/client';
import { useEffect, useRef, useState } from 'react';
const GET_POSTS = gql`
query GetPosts($cursor: String, $limit: Int!) {
posts(cursor: $cursor, limit: $limit) {
edges {
node {
id
title
excerpt
createdAt
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function InfinitePostsList() {
const limit = 10;
const [posts, setPosts] = useState([]);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(true);
const observer = useRef();
const lastPostRef = useRef();
const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
variables: { limit, cursor: null },
notifyOnNetworkStatusChange: true,
});
useEffect(() => {
if (data) {
setPosts(prev => [...prev, ...data.posts.edges.map(edge => edge.node)]);
setCursor(data.posts.pageInfo.endCursor);
setHasMore(data.posts.pageInfo.hasNextPage);
}
}, [data]);
useEffect(() => {
if (loading || !hasMore) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
});
if (lastPostRef.current) {
observer.current.observe(lastPostRef.current);
}
return () => {
if (observer.current) observer.current.disconnect();
};
}, [loading, hasMore, lastPostRef.current]);
const loadMore = () => {
if (!hasMore || loading) return;
fetchMore({
variables: {
cursor,
limit,
},
});
};
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Posts</h2>
<ul>
{posts.map((post, index) => (
<li
key={post.id}
ref={index === posts.length - 1 ? lastPostRef : null}
>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
<small>{new Date(post.createdAt).toLocaleDateString()}</small>
</li>
))}
</ul>
{loading && <p>Loading more posts...</p>}
{!hasMore && <p>No more posts to load</p>}
</div>
);
}
6. Error Handling
Implementing robust error handling with GraphQL clients:
import { useQuery, gql } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
}
}
`;
function ErrorBoundary({ children }) {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const handleError = (error) => {
setHasError(true);
setError(error);
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, []);
if (hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{error?.message || 'Unknown error'}</p>
<button onClick={() => window.location.reload()}>Reload page</button>
</div>
);
}
return children;
}
function PostList() {
const { loading, error, data, refetch } = useQuery(GET_POSTS, {
notifyOnNetworkStatusChange: true,
errorPolicy: 'all', // 'none', 'ignore', or 'all'
});
if (loading) return <p>Loading...</p>;
if (error) {
// Handle different types of errors
if (error.networkError) {
return (
<div className="error-message">
<h3>Network Error</h3>
<p>Please check your internet connection</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
}
if (error.graphQLErrors?.length) {
return (
<div className="error-message">
<h3>GraphQL Error</h3>
<ul>
{error.graphQLErrors.map(({ message, locations, path }, i) => (
<li key={i}>
<p>{message}</p>
<p>Location: {JSON.stringify(locations)}</p>
<p>Path: {path}</p>
</li>
))}
</ul>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
}
return (
<div className="error-message">
<h3>Error</h3>
<p>{error.message}</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
);
}
return (
<div>
<h2>Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
function App() {
return (
<ErrorBoundary>
<PostList />
</ErrorBoundary>
);
}
7. Authentication
Implementing authentication with GraphQL clients:
// lib/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink, concat } from '@apollo/client';
const httpLink = new HttpLink({ uri: 'https://your-graphql-endpoint.com/graphql' });
// Auth middleware to add the token to requests
const authMiddleware = new ApolloLink((operation, forward) => {
// Get the authentication token from local storage
const token = localStorage.getItem('auth_token');
// Add the authorization header to the request
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
}));
return forward(operation);
});
const client = new ApolloClient({
link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
});
export default client;
Implementing a login form:
import { useMutation, gql } from '@apollo/client';
import { useState } from 'react';
const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
name
email
}
}
}
`;
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [login, { loading, error }] = useMutation(LOGIN, {
onCompleted: ({ login }) => {
// Store the token in localStorage
localStorage.setItem('auth_token', login.token);
// Store user info in localStorage or state management
localStorage.setItem('user', JSON.stringify(login.user));
// Redirect to dashboard or home page
window.location.href = '/dashboard';
},
});
const handleSubmit = (e) => {
e.preventDefault();
login({ variables: { email, password } });
};
return (
<div className="login-form">
<h2>Login</h2>
{error && (
<div className="error-message">
{error.message.includes('credentials')
? 'Invalid email or password'
: 'An error occurred. Please try again.'}
</div>
)}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
}
Performance Considerations
1. Query Batching
Batching multiple queries into a single network request:
// Apollo Client setup with query batching
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const client = new ApolloClient({
link: new BatchHttpLink({
uri: 'https://your-graphql-endpoint.com/graphql',
batchMax: 5, // Maximum number of operations per batch
batchInterval: 20, // Wait 20ms to batch operations
}),
cache: new InMemoryCache(),
});
2. Persisted Queries
Reducing network payload by sending query hashes instead of full queries:
// Apollo Client setup with persisted queries
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true,
}).concat(
new HttpLink({ uri: 'https://your-graphql-endpoint.com/graphql' })
);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
3. Cache Normalization
Ensuring efficient cache updates with proper normalization:
// Apollo Client setup with custom cache normalization
import { ApolloClient, InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// Merge function for paginated queries
keyArgs: false,
merge(existing = [], incoming, { args }) {
const { offset = 0 } = args || {};
// Merge the existing and incoming arrays
const merged = existing ? [...existing] : [];
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
Post: {
// Define unique identifier for Post type
keyFields: ['id'],
fields: {
comments: {
// Merge function for nested arrays
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
});
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache,
});
4. Query Deduplication
Avoiding duplicate network requests for the same query:
// Apollo Client setup with query deduplication
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({ uri: 'https://your-graphql-endpoint.com/graphql' }),
cache: new InMemoryCache(),
queryDeduplication: true, // Enable query deduplication (default is true)
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
Choosing the Right GraphQL Client
Comparison Table
Feature | Apollo Client | Relay | URQL | SWR + GraphQL |
---|---|---|---|---|
Bundle Size | Larger | Medium | Small | Small |
Learning Curve | Moderate | Steep | Gentle | Gentle |
Caching | Normalized | Normalized | Configurable | Document-based |
TypeScript Support | Good | Good | Excellent | Good |
Community & Ecosystem | Large | Medium | Growing | Large |
Customization | High | Medium | Very High | High |
SSR Support | Yes | Yes | Yes | Yes |
Local State Management | Yes | No | Via plugins | No |
Optimistic Updates | Built-in | Built-in | Via plugins | Manual |
Best For | Most projects | Large apps | Small-medium apps | Simple data fetching |
Decision Factors
-
Project Size and Complexity:
- Small to medium projects: URQL or SWR
- Large, complex applications: Apollo Client or Relay
-
Bundle Size Concerns:
- If bundle size is critical: URQL or SWR
- If features are more important than size: Apollo Client
-
Team Experience:
- Teams new to GraphQL: Apollo Client or URQL
- Teams with React expertise: Relay
- Teams familiar with React Hooks: SWR
-
Feature Requirements:
- Need for local state management: Apollo Client
- Complex data requirements: Relay
- Customizable caching: URQL
- Simple data fetching with minimal setup: SWR
-
Performance Priorities:
- Maximum performance: Relay
- Balance of performance and developer experience: Apollo Client or URQL
Conclusion
GraphQL client libraries are essential tools for modern web development, providing powerful abstractions for data fetching, caching, and state management. Each library has its strengths and trade-offs, making them suitable for different types of projects and teams.
Apollo Client offers a comprehensive solution with a rich feature set, making it a good default choice for many projects. Relay provides unmatched performance optimizations but comes with a steeper learning curve. URQL strikes a balance between simplicity and power with its modular architecture. SWR with GraphQL offers a lightweight approach focused on data fetching.
When integrating with Next.js, all these libraries can be configured to work with server-side rendering and static site generation, enabling efficient data loading patterns for modern web applications.
By understanding the strengths and weaknesses of each library, you can make an informed decision that best suits your project requirements, team expertise, and performance goals.