GraphQL with Apollo Client: A Complete Guide
GraphQL with Apollo Client: A Complete Guide
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.
Setup and Configuration
Installation
npm install @apollo/client graphql
Basic Setup
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<YourApp />
</ApolloProvider>
);
}
Queries
Basic Query
import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function UsersList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Query with Variables
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
title
}
}
}
`;
function UserProfile({ userId }) {
const { loading, data } = useQuery(GET_USER, {
variables: { id: userId },
});
if (loading) return null;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}
Conditional Queries
const { data } = useQuery(GET_USER, {
variables: { id: userId },
skip: !userId, // Skip query if no userId
fetchPolicy: 'cache-and-network', // Refetch from network
});
Mutations
Basic Mutation
import { gql, useMutation } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(input: { name: $name, email: $email }) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER);
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
await createUser({
variables: {
name: formData.get('name'),
email: formData.get('email'),
},
});
} catch (err) {
console.error(err);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={loading}>
Create User
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
Updating Cache After Mutation
const [createUser] = useMutation(CREATE_USER, {
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`,
});
return [...existingUsers, newUserRef];
},
},
});
},
});
// Or refetch queries
const [createUser] = useMutation(CREATE_USER, {
refetchQueries: [
{ query: GET_USERS },
],
});
Caching
Cache Policies
const { data } = useQuery(GET_USERS, {
fetchPolicy: 'cache-first', // Default - use cache if available
// 'cache-only' - never fetch from network
// 'network-only' - always fetch, don't use cache
// 'no-cache' - don't save to cache
// 'cache-and-network' - show cache, then update with network
});
Cache Configuration
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
// Merge incoming data with existing
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
User: {
keyFields: ['id'], // Use id as unique key
fields: {
name: {
// Transform field
read(name) {
return name?.toUpperCase();
},
},
},
},
},
}),
});
Subscriptions
Setting Up WebSocket
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
});
const wsLink = new WebSocketLink({
uri: 'ws://api.example.com/graphql',
options: {
reconnect: true,
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Using Subscriptions
import { gql, useSubscription } from '@apollo/client';
const USER_UPDATED = gql`
subscription OnUserUpdated {
userUpdated {
id
name
email
}
}
`;
function UserUpdates() {
const { data, loading } = useSubscription(USER_UPDATED);
if (loading) return null;
return <p>User updated: {data.userUpdated.name}</p>;
}
Pagination
Cursor-Based Pagination
const GET_POSTS = gql`
query GetPosts($cursor: String) {
posts(cursor: $cursor, limit: 10) {
edges {
id
title
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostsList() {
const { data, loading, fetchMore } = useQuery(GET_POSTS);
const loadMore = () => {
fetchMore({
variables: {
cursor: data.posts.pageInfo.endCursor,
},
});
};
return (
<div>
{data?.posts.edges.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{data?.posts.pageInfo.hasNextPage && (
<button onClick={loadMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}
Error Handling
const { loading, error, data } = useQuery(GET_USERS, {
onError: (error) => {
console.error('Query error:', error);
},
errorPolicy: 'all', // Show partial data even with errors
});
// Global error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(`GraphQL error: ${message}`)
);
}
if (networkError) {
console.log(`Network error: ${networkError}`);
}
});
Local State Management
// Define local-only field
const GET_LOCAL_DATA = gql`
query GetLocalData {
isLoggedIn @client
currentUser @client {
id
name
}
}
`;
// Update local state
const [updateUser] = useMutation(gql`
mutation UpdateCurrentUser($user: User!) {
updateCurrentUser(user: $user) @client
}
`);
// In cache
const client = new ApolloClient({
cache: new InMemoryCache(),
resolvers: {
Query: {
isLoggedIn: () => !!localStorage.getItem('token'),
currentUser: () => {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
},
},
Mutation: {
updateCurrentUser: (_, { user }, { cache }) => {
cache.writeQuery({
query: GET_LOCAL_DATA,
data: { currentUser: user },
});
return user;
},
},
},
});
Optimistic UI
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}
`;
const [updateUser] = useMutation(UPDATE_USER, {
optimisticResponse: (variables) => ({
updateUser: {
id: variables.id,
name: variables.name,
__typename: 'User',
},
}),
update(cache, { data: { updateUser } }) {
cache.writeFragment({
id: `User:${updateUser.id}`,
fragment: gql`
fragment UpdatedUser on User {
name
}
`,
data: { name: updateUser.name },
});
},
});
Best Practices
- Use fragments for reusable field selections
- Implement proper error handling
- Use optimistic UI for better UX
- Configure cache policies for your needs
- Handle pagination properly
- Use TypeScript with generated types
import { gql, useQuery } from '@apollo/client';
import { GetUsersQuery, GetUsersQueryVariables } from './generated';
const GET_USERS = gql`
query GetUsers {
users {
id
name
}
}
`;
function useUsers() {
return useQuery<GetUsersQuery, GetUsersQueryVariables>(GET_USERS);
}
Conclusion
Apollo Client provides a powerful solution for managing GraphQL data in React applications. Its caching, optimistic UI, and real-time capabilities make it an excellent choice for modern web applications.