Caching is crucial for GraphQL performance. Unlike REST with multiple endpoints, GraphQL has a single endpoint that requires sophisticated caching strategies.
Understanding GraphQL Caching
Cache Identifiers
Apollo Client uses __typename and id fields:
graphql
query GetUser($id: ID!) {
user(id: $id) {
id
__typename
name
email
}
}Cache Storage
javascript
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id']
},
Product: {
keyFields: ['sku']
}
}
})
});Cache Invalidation
Manual Invalidation
javascript
import { useMutation, useQuery } from '@apollo/client';
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}
`;
function UserProfile({ userId }) {
const [updateUser] = useMutation(UPDATE_USER);
const handleUpdate = async (name) => {
await updateUser({
variables: { id: userId, name },
update(cache, { data: { updateUser } }) {
cache.modify({
id: cache.identify({ __typename: 'User', id: userId }),
fields: {
name() {
return updateUser.name;
}
}
});
}
});
};
}Refetch Queries
javascript
const [addComment] = useMutation(ADD_COMMENT, {
refetchQueries: [
{ query: GET_POST_COMMENTS, variables: { postId } }
]
});Optimistic UI
javascript
const [addComment] = useMutation(ADD_COMMENT, {
optimisticResponse: {
addComment: {
__typename: 'Comment',
id: 'temp-id',
content: newComment,
author: currentUser
}
},
update(cache, { data: { addComment } }) {
cache.modify({
id: cache.identify({ __typename: 'Post', id: postId }),
fields: {
comments(existing = []) {
return [...existing, addComment];
}
}
});
}
});Advanced Caching
Cache Redirects
javascript
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
user: {
read(_, { args, toReference }) {
return toReference({
__typename: 'User',
id: args.id
});
}
}
}
}
}
});Field Policy Caching
javascript
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
keyArgs: ['type'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
});Pagination Caching
javascript
const { data, fetchMore } = useQuery(GET_MESSAGES, {
variables: { limit: 10, cursor: null },
notifyOnNetworkStatusChange: true
});
const loadMore = () => {
fetchMore({
variables: {
cursor: data.messages[data.messages.length - 1].cursor
},
updateQuery(previous, { fetchMoreResult }) {
return {
messages: [
...previous.messages,
...fetchMoreResult.messages
]
};
}
});
};Server-Side Caching
Response Caching
javascript
import { responseCachePlugin } from '@apollo/server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
responseCachePlugin({
session: (ctx) => ctx.request.http.headers.get('session-id'),
cache: new RedisCache({
host: 'redis-server'
})
})
]
});DataLoader Pattern
javascript
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findMany({
where: { id: { in: ids } }
});
return ids.map(id =>
users.find(user => user.id === id)
);
});
const resolvers = {
User: {
posts: (user) => {
return postLoader.loadMany(user.postIds);
}
}
};Persisted Queries
javascript
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash/hashing';
import { ApolloLink, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({ uri: '/graphql' });
const persistedLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
});
const client = new ApolloClient({
link: persistedLink.concat(httpLink),
cache: new InMemoryCache()
});Cache Strategies
Cache-First
javascript
const { data } = useQuery(GET_USER, {
fetchPolicy: 'cache-first'
});Network-Only
javascript
const { data } = useQuery(GET_USER, {
fetchPolicy: 'network-only'
});Cache-and-Network
javascript
const { data } = useQuery(GET_USER, {
fetchPolicy: 'cache-and-network'
});Standby Queries
javascript
const { data } = useQuery(GET_DASHBOARD, {
fetchPolicy: 'cache-first',
nextFetchPolicy: 'cache-only',
pollInterval: 30000
});Cache Reset
Complete Reset
javascript
import { useApolloClient } from '@apollo/client';
function LogoutButton() {
const client = useApolloClient();
const logout = () => {
client.resetStore();
// or
client.clearStore();
};
}Selective Eviction
javascript
client.evict({ id: 'User:123' });
client.evict({ fieldName: 'feed' });
client.gc();Monitoring Cache
Cache Performance
javascript
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-first',
errorPolicy: 'all',
notifyOnNetworkStatusChange: true
}
}
});
// Monitor cache hits
client.onCacheStoreWrite(() => {
console.log('Cache updated');
});Cache Inspection
javascript
import { useApolloClient } from '@apollo/client';
function CacheInspector() {
const client = useApolloClient();
const inspect = () => {
console.log(client.cache.extract());
};
return <button onClick={inspect}>Inspect Cache</button>;
}Best Practices
- 1Always include __typename and id in queries
- 2Use optimistic responses for mutations
- 3Implement proper cache invalidation
- 4Use DataLoader for N+1 problems
- 5Set appropriate TTL for server-side caching
- 6Monitor cache hit rates
- 7Use cache-first for frequently accessed data
- 8Network-only for real-time data
Conclusion
Effective caching strategies dramatically improve GraphQL performance. Combine client and server-side caching for optimal results.