GraphQL + Apollo Client Best Practices
Patterns et bonnes pratiques que j'utilise avec Apollo Client dans nos projets React.
Structure des requêtes
Co-localisation avec les composants
// components/UserProfile/UserProfile.tsx
import { gql, useQuery } from '@apollo/client';
// Définis la query à côté du composant qui l'utilise
const USER_PROFILE_QUERY = gql`
query UserProfile($userId: ID!) {
user(id: $userId) {
id
name
email
avatar
# Demande uniquement ce dont tu as besoin
}
}
`;
export function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useQuery(USER_PROFILE_QUERY, {
variables: { userId },
});
// ...
}
Fragments réutilisables
// fragments/UserFields.ts
import { gql } from '@apollo/client';
export const USER_BASIC_FIELDS = gql`
fragment UserBasicFields on User {
id
name
email
avatar
}
`;
export const USER_DETAILED_FIELDS = gql`
fragment UserDetailedFields on User {
...UserBasicFields
bio
location
createdAt
lastActive
}
${USER_BASIC_FIELDS}
`;
Utilisation :
import { USER_DETAILED_FIELDS } from './fragments/UserFields';
const USER_PROFILE_QUERY = gql`
query UserProfile($userId: ID!) {
user(id: $userId) {
...UserDetailedFields
}
}
${USER_DETAILED_FIELDS}
`;
Gestion du cache
Lecture du cache
// Lire une valeur du cache sans faire de requête
function useUserFromCache(userId: string) {
const client = useApolloClient();
const user = client.readFragment({
id: `User:${userId}`,
fragment: USER_BASIC_FIELDS,
});
return user;
}
Mise à jour optimiste
const [updateUser] = useMutation(UPDATE_USER_MUTATION, {
optimisticResponse: {
updateUser: {
__typename: 'User',
id: userId,
name: newName, // Nouvelle valeur
},
},
update(cache, { data }) {
// Mettre à jour le cache après la mutation
cache.modify({
id: cache.identify({ __typename: 'User', id: userId }),
fields: {
name() {
return data?.updateUser.name;
},
},
});
},
});
Cache eviction
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
update(cache, { data }) {
const deletedPostId = data?.deletePost.id;
// Retirer l'objet du cache
cache.evict({ id: `Post:${deletedPostId}` });
cache.gc(); // Garbage collect
},
});
Error Handling
Hook personnalisé pour gérer les erreurs
import { ApolloError } from '@apollo/client';
import { useCallback } from 'react';
export function useGraphQLErrorHandler() {
return useCallback((error: ApolloError) => {
// Erreurs réseau
if (error.networkError) {
console.error('Network error:', error.networkError);
// Afficher un toast ou notification
return;
}
// Erreurs GraphQL
if (error.graphQLErrors) {
error.graphQLErrors.forEach(({ message, extensions }) => {
// Gérer selon le code d'erreur
switch (extensions?.code) {
case 'UNAUTHENTICATED':
// Rediriger vers login
break;
case 'FORBIDDEN':
// Afficher message d'accès refusé
break;
default:
console.error('GraphQL error:', message);
}
});
}
}, []);
}
Utilisation :
function MyComponent() {
const handleError = useGraphQLErrorHandler();
const { data, error } = useQuery(MY_QUERY, {
onError: handleError,
});
// ...
}
Pagination
Cursor-based pagination
const POSTS_QUERY = gql`
query Posts($after: String, $first: Int = 10) {
posts(after: $after, first: $first) {
edges {
cursor
node {
id
title
content
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostList() {
const { data, loading, fetchMore } = useQuery(POSTS_QUERY);
const loadMore = () => {
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],
},
};
},
});
};
return (
<div>
{data?.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{data?.posts.pageInfo.hasNextPage && (
<button onClick={loadMore}>Load more</button>
)}
</div>
);
}
Policies de cache
Configuration Apollo Client
import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Merge strategy pour les listes paginées
posts: {
keyArgs: ['filter'], // Cache séparé par filtre
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
User: {
fields: {
// Computed field local
fullName: {
read(_, { readField }) {
const firstName = readField('firstName');
const lastName = readField('lastName');
return `${firstName} ${lastName}`;
},
},
},
},
},
});
Local State Management
Client-only fields
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
isSelected: {
read(existing = false) {
return existing;
},
},
},
},
},
});
// Mise à jour
cache.writeFragment({
id: `User:${userId}`,
fragment: gql`
fragment UserSelection on User {
isSelected
}
`,
data: {
isSelected: true,
},
});
Performance Tips
1. Utiliser useLazyQuery pour les requêtes à la demande
const [searchUsers, { data, loading }] = useLazyQuery(SEARCH_USERS_QUERY);
// Appeler uniquement quand nécessaire
const handleSearch = (term: string) => {
searchUsers({ variables: { term } });
};
2. Batching des requêtes
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const link = new BatchHttpLink({
uri: '/graphql',
batchMax: 10, // Max 10 requêtes par batch
batchInterval: 20, // Attendre 20ms avant d'envoyer
});
3. Skip les requêtes conditionnellement
const { data } = useQuery(USER_QUERY, {
variables: { userId },
skip: !userId, // Ne pas exécuter si pas d'userId
});
4. Utiliser fetchPolicy intelligemment
// Cache-first (défaut) - bon pour les données statiques
useQuery(QUERY, { fetchPolicy: 'cache-first' });
// Network-only - pour les données temps réel
useQuery(QUERY, { fetchPolicy: 'network-only' });
// Cache-and-network - meilleur UX
useQuery(QUERY, { fetchPolicy: 'cache-and-network' });
Testing
Mock Apollo Provider
import { MockedProvider } from '@apollo/client/testing';
const mocks = [
{
request: {
query: USER_QUERY,
variables: { userId: '1' },
},
result: {
data: {
user: {
id: '1',
name: 'John Doe',
email: 'john@example.com',
},
},
},
},
];
test('renders user profile', async () => {
render(
<MockedProvider mocks={mocks}>
<UserProfile userId="1" />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
Checklist de bonnes pratiques
- ✅ Utiliser des fragments pour réutiliser les champs
- ✅ Co-localiser les queries avec les composants
- ✅ Gérer les erreurs de manière centralisée
- ✅ Optimiser les updates de cache avec
optimisticResponse - ✅ Utiliser
fetchPolicyappropriée selon le cas - ✅ Typer les queries/mutations avec TypeScript (via codegen)
- ✅ Éviter les over-fetching (ne demander que ce dont on a besoin)
- ✅ Implémenter la pagination pour les listes longues
- ✅ Nettoyer le cache avec
evictquand nécessaire