GraphQL provides an efficient alternative to REST for data fetching in React applications. Apollo Client is the most popular GraphQL client for React. Here’s a comprehensive guide to integrating them:
1. Setup and Configuration
Installation
npm install @apollo/client graphql
# or
yarn add @apollo/client graphql
Apollo Client Setup
// src/apollo-client.js
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: 'https://your-graphql-endpoint.com/graphql',
});
const authLink = setContext((_, { headers }) => {
// Get authentication token from storage if it exists
const token = localStorage.getItem('authToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
export default client;
Wrap Your App with ApolloProvider
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import client from './apollo-client';
import { ApolloProvider } from '@apollo/client';
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
2. Basic Queries
useQuery Hook
import { useQuery, gql } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
body
author {
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 (
<ul>
{data.posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<p>By: {post.author.name}</p>
</li>
))}
</ul>
);
}
Query with Variables
const GET_POST = gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
body
}
}
`;
function PostDetail({ postId }) {
const { loading, error, data } = useQuery(GET_POST, {
variables: { id: postId },
});
// ...render logic
}
3. Mutations
useMutation Hook
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $body: String!) {
createPost(title: $title, body: $body) {
id
title
body
}
}
`;
function CreatePostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST);
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const { data } = await createPost({
variables: { title, body },
});
console.log('Post created:', data.createPost);
// Reset form or redirect
} catch (err) {
console.error('Error creating post:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Body"
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
Optimistic UI Updates
const [createPost] = useMutation(CREATE_POST, {
optimisticResponse: {
__typename: 'Mutation',
createPost: {
__typename: 'Post',
id: 'temp-id',
title,
body,
},
},
update: (cache, { data: { createPost } }) => {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: createPost,
fragment: gql`
fragment NewPost on Post {
id
title
body
}
`
});
return [...existingPosts, newPostRef];
},
},
});
},
});
4. Advanced Features
Pagination
const GET_POSTS = gql`
query GetPosts($first: Int, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostList() {
const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 5 },
});
const loadMore = () => {
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor,
},
});
};
// ...render logic
return (
<div>
{/* Post list */}
{data?.posts?.pageInfo.hasNextPage && (
<button onClick={loadMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}
Local State Management
// Add to your Apollo Client setup
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
typeDefs: gql`
extend type Query {
isLoggedIn: Boolean!
}
`,
resolvers: {
Query: {
isLoggedIn: () => !!localStorage.getItem('authToken'),
},
},
});
// Set initial state
client.writeQuery({
query: gql`
query GetAuthStatus {
isLoggedIn
}
`,
data: {
isLoggedIn: !!localStorage.getItem('authToken'),
},
});
// Usage in components
const { data } = useQuery(gql`
query GetAuthStatus {
isLoggedIn @client
}
`);
5. Authentication
Login Mutation
const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
name
}
}
}
`;
function LoginForm() {
const [login, { loading, error }] = useMutation(LOGIN, {
onCompleted: ({ login }) => {
localStorage.setItem('authToken', login.token);
client.writeQuery({
query: gql`
query GetAuthStatus {
isLoggedIn
}
`,
data: { isLoggedIn: true },
});
},
});
// ...form handling
}
Protected Routes
import { useQuery } from '@apollo/client';
const IS_LOGGED_IN = gql`
query IsLoggedIn {
isLoggedIn @client
}
`;
function ProtectedRoute({ children }) {
const { data } = useQuery(IS_LOGGED_IN);
if (!data?.isLoggedIn) {
return <Navigate to="/login" replace />;
}
return children;
}
6. Error Handling
Global Error Handling
import { ApolloClient, from, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
if (message.includes('Unauthorized')) {
// Handle token expiration
}
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
const client = new ApolloClient({
link: from([errorLink, authLink.concat(httpLink)]),
cache: new InMemoryCache(),
});
UI Error Boundaries
import { useQuery } from '@apollo/client';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function QueryWithErrorBoundary() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => window.location.reload()}
>
<PostList />
</ErrorBoundary>
);
}
Best Practices
- Code Organization:
- Keep queries/mutations in separate files
- Use fragments for shared fields
- Organize by feature rather than by type
- Performance:
- Use
fetchPolicy
wisely (cache-first is default) - Implement pagination for large datasets
- Use
@defer
for non-critical data
- Testing:
- Mock Apollo Client in tests
- Use
MockedProvider
for component tests
- Security:
- Never store sensitive data in client state
- Implement proper authentication
- Use persisted queries in production
- Developer Experience:
- Use GraphQL Code Generator for TypeScript types
- Set up GraphQL linting
- Document your schema thoroughly
This comprehensive integration of Apollo Client with React provides a solid foundation for building performant, type-safe applications with GraphQL. The combination of declarative data fetching, real-time updates, and local state management makes Apollo Client a powerful tool for modern React development.