GraphQL Client Development Guide
GraphQL client development is a key part of building modern frontend applications. Here is a comprehensive guide to using Apollo Client and other GraphQL clients.
1. Apollo Client Configuration
Basic Configuration
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
credentials: 'include'
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all'
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all'
},
mutate: {
errorPolicy: 'all'
}
}
});
Configuration with Authentication
import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql'
});
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
2. Querying Data
Using useQuery Hook
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
name
email
createdAt
}
}
`;
function UserList() {
const { loading, error, data, fetchMore } = useQuery(GET_USERS, {
variables: { limit: 10, offset: 0 },
notifyOnNetworkStatusChange: true
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
<button
onClick={() => fetchMore({
variables: { offset: data.users.length },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
users: [...prev.users, ...fetchMoreResult.users]
};
}
})}
>
Load More
</button>
</div>
);
}
Using Lazy Query
import { useLazyQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserSearch() {
const [getUser, { loading, error, data }] = useLazyQuery(GET_USER);
const [userId, setUserId] = useState('');
return (
<div>
<input
value={userId}
onChange={(e) => setUserId(e.target.value)}
placeholder="Enter user ID"
/>
<button onClick={() => getUser({ variables: { id: userId } })}>
Search
</button>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && (
<div>
<h3>{data.user.name}</h3>
<p>{data.user.email}</p>
</div>
)}
</div>
);
}
3. Mutating Data
Using useMutation Hook
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = 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];
}
}
});
}
});
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
createUser({
variables: {
input: { name, email }
}
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}
Optimistic Updates
const [updateUser, { loading }] = useMutation(UPDATE_USER, {
optimisticResponse: (variables) => ({
updateUser: {
__typename: 'User',
id: variables.id,
name: variables.input.name,
email: variables.input.email
}
}),
update(cache, { data: { updateUser } }) {
cache.writeFragment({
id: `User:${updateUser.id}`,
fragment: gql`
fragment UpdateUser on User {
name
email
}
`,
data: updateUser
});
}
});
4. Cache Management
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
keyArgs: ['filter', 'sort'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
},
read(existing, { args: { offset, limit } }) {
return existing && existing.slice(offset, offset + limit);
}
},
user: {
read(_, { args, toReference }) {
return toReference({
__typename: 'User',
id: args.id
});
}
}
}
},
User: {
keyFields: ['id', 'email']
}
}
});
Manual Cache Updates
import { useApolloClient } from '@apollo/client';
function UpdateUserButton({ userId, newData }) {
const client = useApolloClient();
const handleClick = () => {
client.writeFragment({
id: `User:${userId}`,
fragment: gql`
fragment UserFragment on User {
name
email
}
`,
data: newData
});
};
return <button onClick={handleClick}>Update User</button>;
}
Clear Cache
function ClearCacheButton() {
const client = useApolloClient();
const handleClick = () => {
client.clearStore();
};
return <button onClick={handleClick}>Clear Cache</button>;
}
const GET_POSTS = gql`
query GetPosts($offset: Int, $limit: Int) {
posts(offset: $offset, limit: $limit) {
id
title
content
author {
name
}
}
}
`;
function PostList() {
const { loading, data, fetchMore } = useQuery(GET_POSTS, {
variables: { offset: 0, limit: 10 }
});
return (
<div>
{data?.posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
<button
onClick={() => fetchMore({
variables: { offset: data.posts.length },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: [...prev.posts, ...fetchMoreResult.posts]
};
}
})}
>
Load More
</button>
</div>
);
}
const GET_POSTS = gql`
query GetPosts($after: String, $first: Int) {
posts(after: $after, first: $first) {
edges {
node {
id
title
content
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostList() {
const { loading, data, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 10 }
});
return (
<div>
{data?.posts.edges.map(({ node }) => (
<div key={node.id}>
<h3>{node.title}</h3>
<p>{node.content}</p>
</div>
))}
{data?.posts.pageInfo.hasNextPage && (
<button
onClick={() => fetchMore({
variables: { after: data.posts.pageInfo.endCursor },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: {
...fetchMoreResult.posts,
edges: [
...prev.posts.edges,
...fetchMoreResult.posts.edges
]
}
};
}
})}
>
Load More
</button>
)}
</div>
);
}
6. Error Handling
Handling GraphQL Errors
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <div>Loading...</div>;
if (error) {
if (error.graphQLErrors) {
return (
<div>
GraphQL Errors:
{error.graphQLErrors.map((err, i) => (
<div key={i}>{err.message}</div>
))}
</div>
);
}
if (error.networkError) {
return <div>Network Error: {error.networkError.message}</div>;
}
return <div>Error: {error.message}</div>;
}
return <div>{/* render data */}</div>;
}
Global Error Handling
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql'
});
const client = new ApolloClient({
link: errorLink.concat(httpLink),
cache: new InMemoryCache()
});
7. Subscriptions
Using useSubscription Hook
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_ADDED = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
text
author {
name
}
createdAt
}
}
`;
function ChatRoom({ roomId }) {
const { data, loading, error } = useSubscription(MESSAGE_ADDED, {
variables: { roomId }
});
if (loading) return <div>Connecting...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>New Message:</h3>
<p>{data.messageAdded.text}</p>
<small>By {data.messageAdded.author.name}</small>
</div>
);
}
Using Persisted Queries
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
});
const client = new ApolloClient({
link: persistedQueryLink.concat(httpLink),
cache: new InMemoryCache()
});
Batch Queries
import { useQuery, gql } from '@apollo/client';
const GET_MULTIPLE_USERS = gql`
query GetMultipleUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
email
}
}
`;
function UserList({ userIds }) {
const { loading, data } = useQuery(GET_MULTIPLE_USERS, {
variables: { ids: userIds }
});
if (loading) return <div>Loading...</div>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
9. Client Development Best Practices
| Practice | Description |
|---|
| Use caching | Reduce network requests, improve performance |
| Implement optimistic updates | Improve user experience |
| Handle errors | Provide friendly error messages |
| Use pagination | Handle large datasets |
| Implement loading states | Improve user experience |
| Use subscriptions | Enable real-time updates |
| Optimize queries | Request only needed fields |
| Use persisted queries | Reduce network transmission |
| Implement retry mechanisms | Improve reliability |
| Monitor performance | Detect performance issues in time |
10. Common Issues and Solutions
| Issue | Cause | Solution |
|---|
| Cache not updating | Cache policy misconfigured | Adjust cache policy, manually update cache |
| Slow queries | Requesting too much data | Optimize queries, use field selection |
| High memory usage | Too much cached data | Regularly clean cache, limit cache size |
| Subscription disconnected | Unstable network | Implement auto-reconnect mechanism |
| Poor error handling | Errors not handled properly | Implement global error handling |