| name | apollo-client |
| description | [Applies to: **/*.{js,jsx}] This guide defines the definitive best practices for using Apollo Client 3.x in our React applications, focusing on cache hygiene, type safety, performance, and maintainability. |
| source | cursor_mdc |
apollo-client Best Practices
Apollo Client is our standard for GraphQL state management. Adhering to these guidelines ensures predictable data flow, optimal performance, and a scalable codebase.
1. Client Setup & Initialization
Always instantiate ApolloClient as a singleton. Avoid creating multiple client instances, as each client manages its own InMemoryCache, leading to inconsistent state and wasted resources.
❌ BAD: Multiple ApolloClient instances
const client = new ApolloClient({ });
✅ GOOD: Singleton ApolloClient
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
const httpLink = new HttpLink({
uri: process.env.GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
headers: {
authorization: localStorage.getItem('token') || '',
'client-name': 'OurApp [web]',
'client-version': '1.0.0',
},
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
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 retryLink = new RetryLink({
delay: {
initial: 300,
max: Infinity,
jitter: true
},
attempts: {
max: 5,
retryIf: (error, _operation) => !!error
}
});
export const client = new ApolloClient({
link: from([errorLink, retryLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Query: {
fields: {
users: {
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
persistedQueries: {
ttl: 3600,
},
});
import { ApolloProvider } from '@apollo/client';
import { client } from './apolloClient';
function App() {
return (
<ApolloProvider client={client}>
{/* Your application */}
</ApolloProvider>
);
}
2. Data Fetching with React Hooks
Always use Apollo Client's React hooks (useQuery, useMutation, useSubscription) for data operations within components. These hooks provide declarative data fetching, automatic UI updates, and robust state management. Avoid direct client.query() or client.mutate() calls in components unless for specific server-side rendering or pre-fetching scenarios outside the React lifecycle.
❌ BAD: Imperative data fetching in components
import { client } from '../apolloClient';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
client.query({ query: GET_USERS }).then(res => setData(res.data));
}, []);
}
✅ GOOD: Declarative data fetching with useQuery
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
}
}
`;
function UserList({ limit }) {
const { loading, error, data } = useQuery(GET_USERS, {
variables: { limit },
fetchPolicy: 'cache-and-network',
});
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
3. Cache Hygiene & Management
Prioritize immutable cache updates and configure InMemoryCache with TypePolicy and FieldPolicy. This prevents stale data, ensures garbage collection, and customizes merging logic for complex data structures.
fetchPolicy Strategy:
cache-first (default): Use for stable data that rarely changes.
cache-and-network: Use for data that might change frequently, providing instant UI feedback while fetching fresh data.
network-only: Use for critical, real-time data or mutations where the latest server state is paramount.
no-cache: Use for one-off data (e.g., authentication tokens) that should never be stored in the cache.
❌ BAD: Mutating cache objects directly or neglecting keyFields
cache.data.data.User:123.name = 'New Name';
✅ GOOD: Immutable cache updates with cache.modify or writeFragment
import { useMutation, gql } from '@apollo/client';
const UPDATE_USER_NAME = gql`
mutation UpdateUserName($id: ID!, $name: String!) {
updateUserName(id: $id, name: $name) {
id
name
}
}
`;
function UserProfile({ userId, currentName }) {
const [updateName] = useMutation(UPDATE_USER_NAME);
const handleUpdate = async (newName) => {
await updateName({
variables: { id: userId, name: newName },
update: (cache, { data: { updateUserName } }) => {
cache.modify({
id: cache.identify(updateUserName),
fields: {
name() {
return updateUserName.name;
},
},
});
},
});
};
return (
<div>
<p>Current Name: {currentName}</p>
<button onClick={() => handleUpdate('Jane Doe')}>Change Name</button>
</div>
);
}
4. Type Safety & Code Generation
Mandate @graphql-codegen for all GraphQL operations. This generates TypeScript types for queries, mutations, and subscriptions, along with typed React hooks, ensuring end-to-end type safety and reducing runtime errors.
❌ BAD: Manual type definitions or any
interface UserData {
users: Array<{ id: string; name: string }>;
}
const { data } = useQuery<UserData>(GET_USERS);
✅ GOOD: Generated types and hooks
module.exports = {
schema: 'http://localhost:4000/graphql',
documents: 'src/**/*.graphql',
overwrite: true,
generates: {
'./src/generated/graphql.tsx': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
withHooks: true,
withHOC: false,
withComponent: false,
},
},
},
};
import { useGetUsersQuery } from '../generated/graphql';
function UserList({ limit }) {
const { loading, error, data } = useGetUsersQuery({ variables: { limit } });
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data?.users.map(user => ( // Data is now fully typed
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
5. Error Handling & Resilience
Implement centralized error handling using Apollo Link's onError and RetryLink. This provides a consistent way to log, display, and recover from network and GraphQL errors.
❌ BAD: Inconsistent error handling across components
if (error) {
alert(error.message);
}
✅ GOOD: Centralized error handling with onError link (see Section 1 for example) and component-level display.
function MyComponent() {
const { loading, error, data } = useQuery(MY_QUERY);
if (loading) return <p>Loading...</p>;
if (error) {
return <p className="error-message">Failed to load data: {error.message}</p>;
}
}
6. Code Organization & Naming Conventions
Colocate GraphQL documents (.graphql files) with the components that use them. Follow GraphQL schema naming conventions (camelCase for fields/arguments, PascalCase for types, SCREAMING_SNAKE_CASE for enum values) and enforce them with GraphOS schema linting.
❌ BAD: Inline GraphQL strings, generic naming
const MY_HUGE_QUERY = gql`query { /* ... */ }`;
✅ GOOD: Dedicated GraphQL files, clear naming
// src/components/UserList/
// ├── UserList.tsx
// └── UserList.graphql
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
firstName
lastName
}
}
7. Performance Considerations
Utilize fragments for query reuse and to avoid over-fetching. Consider Automatic Persisted Queries (APQ) for production environments to reduce network payload sizes. Optimize fetchPolicy based on data volatility.
❌ BAD: Repeated field selections, large monolithic queries
query GetUserAndPosts {
user(id: "1") { id name email }
posts(userId: "1") { id title content }
}
✅ GOOD: Use fragments for modularity and reuse
fragment UserDetails on User {
id
name
email
}
query GetUserAndPosts($userId: ID!) {
user(id: $userId) {
...UserDetails
}
posts(userId: $userId) {
id
title
content
}
}
8. Testing Approaches
Use @apollo/client/testing's MockedProvider for unit and integration tests of components that use Apollo hooks. This allows you to mock GraphQL operations and control the data returned, ensuring consistent and isolated tests.
❌ BAD: Relying on a live GraphQL API for component tests
render(<MyComponent />);
✅ GOOD: Mocking GraphQL responses with MockedProvider
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { GET_USERS } from './UserList.graphql';
import UserList from './UserList';
const mocks = [
{
request: {
query: GET_USERS,
variables: { limit: 10 },
},
result: {
data: {
users: [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
],
},
},
},
];
test('renders user list', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserList limit={10} />
</MockedProvider>
);
expect(screen.getByText(/loading users.../i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});