@apollo/client 4.1.7 → 4.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/__cjs/react/hooks/useSubscription.cjs +1 -1
- package/__cjs/react/hooks/useSubscription.cjs.map +1 -1
- package/__cjs/react/hooks/useSubscription.d.cts +2 -2
- package/__cjs/version.cjs +1 -1
- package/package.json +3 -2
- package/react/hooks/useSubscription.d.ts +2 -2
- package/react/hooks/useSubscription.js +1 -1
- package/react/hooks/useSubscription.js.map +1 -1
- package/react/hooks-compiled/useSubscription.d.ts +2 -2
- package/react/hooks-compiled/useSubscription.js +1 -1
- package/react/hooks-compiled/useSubscription.js.map +1 -1
- package/skills/apollo-client/SKILL.md +168 -0
- package/skills/apollo-client/references/caching.md +560 -0
- package/skills/apollo-client/references/error-handling.md +344 -0
- package/skills/apollo-client/references/fragments.md +804 -0
- package/skills/apollo-client/references/integration-client.md +340 -0
- package/skills/apollo-client/references/integration-nextjs.md +325 -0
- package/skills/apollo-client/references/integration-react-router.md +256 -0
- package/skills/apollo-client/references/integration-tanstack-start.md +378 -0
- package/skills/apollo-client/references/mutations.md +549 -0
- package/skills/apollo-client/references/queries.md +416 -0
- package/skills/apollo-client/references/state-management.md +428 -0
- package/skills/apollo-client/references/suspense-hooks.md +773 -0
- package/skills/apollo-client/references/troubleshooting.md +487 -0
- package/skills/apollo-client/references/typescript-codegen.md +133 -0
- package/version.js +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apollo-client
|
|
3
|
+
description: >
|
|
4
|
+
Guide for building React applications with Apollo Client 4.x. Use this skill when:
|
|
5
|
+
(1) setting up Apollo Client in a React project,
|
|
6
|
+
(2) writing GraphQL queries or mutations with hooks,
|
|
7
|
+
(3) configuring caching or cache policies,
|
|
8
|
+
(4) managing local state with reactive variables,
|
|
9
|
+
(5) troubleshooting Apollo Client errors or performance issues.
|
|
10
|
+
license: MIT
|
|
11
|
+
compatibility: React 18+, React 19 (Suspense/RSC). Works with Next.js, Vite, CRA, and other React frameworks.
|
|
12
|
+
metadata:
|
|
13
|
+
author: apollographql
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
allowed-tools: Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Apollo Client 4.x Guide
|
|
19
|
+
|
|
20
|
+
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Version 4.x brings improved caching, better TypeScript support, and React 19 compatibility.
|
|
21
|
+
|
|
22
|
+
## Integration Guides
|
|
23
|
+
|
|
24
|
+
Choose the integration guide that matches your application setup:
|
|
25
|
+
|
|
26
|
+
- **[Client-Side Apps](references/integration-client.md)** - For client-side React applications without SSR (Vite, Create React App, etc.)
|
|
27
|
+
- **[Next.js App Router](references/integration-nextjs.md)** - For Next.js applications using the App Router with React Server Components
|
|
28
|
+
- **[React Router Framework Mode](references/integration-react-router.md)** - For React Router 7 applications with streaming SSR
|
|
29
|
+
- **[TanStack Start](references/integration-tanstack-start.md)** - For TanStack Start applications with modern routing
|
|
30
|
+
|
|
31
|
+
Each guide includes installation steps, configuration, and framework-specific patterns optimized for that environment.
|
|
32
|
+
|
|
33
|
+
## Quick Reference
|
|
34
|
+
|
|
35
|
+
### Basic Query
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { gql } from "@apollo/client";
|
|
39
|
+
import { useQuery } from "@apollo/client/react";
|
|
40
|
+
|
|
41
|
+
const GET_USER = gql`
|
|
42
|
+
query GetUser($id: ID!) {
|
|
43
|
+
user(id: $id) {
|
|
44
|
+
id
|
|
45
|
+
name
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
51
|
+
const { loading, error, data, dataState } = useQuery(GET_USER, {
|
|
52
|
+
variables: { id: userId },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (loading) return <p>Loading...</p>;
|
|
56
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
57
|
+
|
|
58
|
+
// TypeScript note: for stricter type narrowing, you can also check `dataState === "complete"` before accessing data
|
|
59
|
+
return <div>{data?.user.name}</div>;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Basic Mutation
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { gql } from "@apollo/client";
|
|
67
|
+
import { useMutation } from "@apollo/client/react";
|
|
68
|
+
|
|
69
|
+
const CREATE_USER = gql`
|
|
70
|
+
mutation CreateUser($input: CreateUserInput!) {
|
|
71
|
+
createUser(input: $input) {
|
|
72
|
+
id
|
|
73
|
+
name
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
function CreateUserForm() {
|
|
79
|
+
const [createUser, { loading, error }] = useMutation(CREATE_USER);
|
|
80
|
+
|
|
81
|
+
const handleSubmit = async (name: string) => {
|
|
82
|
+
await createUser({ variables: { input: { name } } });
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return <button onClick={() => handleSubmit("John")}>Create User</button>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Suspense Query
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { Suspense } from "react";
|
|
93
|
+
import { useSuspenseQuery } from "@apollo/client/react";
|
|
94
|
+
|
|
95
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
96
|
+
const { data } = useSuspenseQuery(GET_USER, {
|
|
97
|
+
variables: { id: userId },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return <div>{data.user.name}</div>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function App() {
|
|
104
|
+
return (
|
|
105
|
+
<Suspense fallback={<p>Loading user...</p>}>
|
|
106
|
+
<UserProfile userId="1" />
|
|
107
|
+
</Suspense>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Reference Files
|
|
113
|
+
|
|
114
|
+
Detailed documentation for specific topics:
|
|
115
|
+
|
|
116
|
+
- [TypeScript Code Generation](references/typescript-codegen.md) - GraphQL Code Generator setup for type-safe operations
|
|
117
|
+
- [Queries](references/queries.md) - useQuery, useLazyQuery, polling, refetching
|
|
118
|
+
- [Suspense Hooks](references/suspense-hooks.md) - useSuspenseQuery, useBackgroundQuery, useReadQuery, useLoadableQuery
|
|
119
|
+
- [Mutations](references/mutations.md) - useMutation, optimistic UI, cache updates
|
|
120
|
+
- [Fragments](references/fragments.md) - Fragment colocation, useFragment, useSuspenseFragment, data masking
|
|
121
|
+
- [Caching](references/caching.md) - InMemoryCache, typePolicies, cache manipulation
|
|
122
|
+
- [State Management](references/state-management.md) - Reactive variables, local state
|
|
123
|
+
- [Error Handling](references/error-handling.md) - Error policies, error links, retries
|
|
124
|
+
- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions
|
|
125
|
+
|
|
126
|
+
## Key Rules
|
|
127
|
+
|
|
128
|
+
### Query Best Practices
|
|
129
|
+
|
|
130
|
+
- **Each page should generally only have one query, composed from colocated fragments.** Use `useFragment` or `useSuspenseFragment` in all non-page-components. Use `@defer` to allow slow fields below the fold to stream in later and avoid blocking the page load.
|
|
131
|
+
- **Fragments are for colocation, not reuse.** Each fragment should describe exactly the data needs of a specific component, not be shared across components for common fields. See [Fragments reference](references/fragments.md) for details on fragment colocation and data masking.
|
|
132
|
+
- Always handle `loading` and `error` states in UI when using non-suspenseful hooks (`useQuery`, `useLazyQuery`). When using Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`), React handles this through `<Suspense>` boundaries and error boundaries.
|
|
133
|
+
- Use `fetchPolicy` to control cache behavior per query
|
|
134
|
+
- Use the TypeScript type server to look up documentation for functions and options (Apollo Client has extensive docblocks)
|
|
135
|
+
|
|
136
|
+
### Mutation Best Practices
|
|
137
|
+
|
|
138
|
+
- **If the schema permits, mutation return values should return everything necessary to update the cache.** Neither manual updates nor refetching should be necessary.
|
|
139
|
+
- If the mutation response is insufficient, carefully weigh manual cache manipulation vs refetching. Manual updates risk missing server logic. Consider optimistic updates with a granular refetch if needed.
|
|
140
|
+
- Handle errors gracefully in the UI
|
|
141
|
+
- Use `refetchQueries` sparingly (prefer letting the cache update automatically)
|
|
142
|
+
|
|
143
|
+
### Caching Best Practices
|
|
144
|
+
|
|
145
|
+
- Configure `keyFields` for types without `id` field
|
|
146
|
+
- Disable normalization by setting `keyFields: false` for types that don't include an identifier and are meant to group related fields under the parent
|
|
147
|
+
- Use `typePolicies` for pagination and computed fields
|
|
148
|
+
- Understand cache normalization to debug issues
|
|
149
|
+
- **Enable [data masking](references/fragments.md#data-masking) for all new applications** - it prevents components from accessing fragment data they don't own, enforcing proper data boundaries and preventing over-rendering
|
|
150
|
+
|
|
151
|
+
### Performance
|
|
152
|
+
|
|
153
|
+
- Avoid over-fetching with proper field selection
|
|
154
|
+
- Configure appropriate `fetchPolicy` per use case
|
|
155
|
+
- Use `@defer` for incremental delivery of deferred query parts, and `@stream` for streaming list fields (`@stream` available in Apollo Client 4.1+)
|
|
156
|
+
- **Prefer Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications** for better loading state handling and code simplicity
|
|
157
|
+
|
|
158
|
+
## Ground Rules
|
|
159
|
+
|
|
160
|
+
- ALWAYS use Apollo Client 4.x patterns (not v3 or earlier)
|
|
161
|
+
- ALWAYS wrap your app with `ApolloProvider`
|
|
162
|
+
- ALWAYS handle loading and error states when using non-suspenseful hooks
|
|
163
|
+
- PREFER Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications for better DX
|
|
164
|
+
- NEVER store Apollo Client in React state (use module-level or context)
|
|
165
|
+
- PREFER `cache-first` for read-heavy data, `network-only` for real-time data
|
|
166
|
+
- USE TypeScript for better type safety with GraphQL
|
|
167
|
+
- IMPLEMENT proper cache updates instead of refetching entire queries
|
|
168
|
+
- ADVISE the human steering you to look into Apollo DevTools when collaboratively debugging Apollo Client issues
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# Caching Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [InMemoryCache Setup](#inmemorycache-setup)
|
|
6
|
+
- [Cache Normalization](#cache-normalization)
|
|
7
|
+
- [Type Policies](#type-policies)
|
|
8
|
+
- [Field Policies](#field-policies)
|
|
9
|
+
- [Pagination](#pagination)
|
|
10
|
+
- [Cache Manipulation](#cache-manipulation)
|
|
11
|
+
- [Garbage Collection](#garbage-collection)
|
|
12
|
+
|
|
13
|
+
## InMemoryCache Setup
|
|
14
|
+
|
|
15
|
+
### Basic Configuration
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { InMemoryCache } from "@apollo/client";
|
|
19
|
+
|
|
20
|
+
const cache = new InMemoryCache({
|
|
21
|
+
// Custom type policies
|
|
22
|
+
typePolicies: {
|
|
23
|
+
Query: {
|
|
24
|
+
fields: {
|
|
25
|
+
// Query-level field policies
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
User: {
|
|
29
|
+
keyFields: ["id"],
|
|
30
|
+
fields: {
|
|
31
|
+
// User-level field policies
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Custom type name handling (rare)
|
|
37
|
+
possibleTypes: {
|
|
38
|
+
Character: ["Human", "Droid"],
|
|
39
|
+
Node: ["User", "Post", "Comment"],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Constructor Options
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
new InMemoryCache({
|
|
48
|
+
// Define how types are identified in cache
|
|
49
|
+
typePolicies: {
|
|
50
|
+
/* ... */
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Interface/union type mappings between supertypes and their subtypes
|
|
54
|
+
possibleTypes: {
|
|
55
|
+
/* ... */
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Custom function to generate cache IDs (rare)
|
|
59
|
+
dataIdFromObject: (object) => {
|
|
60
|
+
if (object.__typename === "Book") {
|
|
61
|
+
return `Book:${object.isbn}`;
|
|
62
|
+
}
|
|
63
|
+
return defaultDataIdFromObject(object);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Cache Normalization
|
|
69
|
+
|
|
70
|
+
Apollo Client normalizes data by splitting query results into individual objects and storing them by unique identifier.
|
|
71
|
+
|
|
72
|
+
### How Normalization Works
|
|
73
|
+
|
|
74
|
+
```graphql
|
|
75
|
+
# Query
|
|
76
|
+
query GetPost {
|
|
77
|
+
post(id: "1") {
|
|
78
|
+
id
|
|
79
|
+
title
|
|
80
|
+
author {
|
|
81
|
+
id
|
|
82
|
+
name
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Normalized cache structure
|
|
90
|
+
{
|
|
91
|
+
'Post:1': {
|
|
92
|
+
__typename: 'Post',
|
|
93
|
+
id: '1',
|
|
94
|
+
title: 'Hello World',
|
|
95
|
+
author: { __ref: 'User:42' }
|
|
96
|
+
},
|
|
97
|
+
'User:42': {
|
|
98
|
+
__typename: 'User',
|
|
99
|
+
id: '42',
|
|
100
|
+
name: 'John'
|
|
101
|
+
},
|
|
102
|
+
ROOT_QUERY: {
|
|
103
|
+
'post({"id":"1"})': { __ref: 'Post:1' }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Benefits of Normalization
|
|
109
|
+
|
|
110
|
+
1. **Automatic updates**: When `User:42` is updated anywhere, all components showing that user update
|
|
111
|
+
2. **Deduplication**: Same objects aren't stored multiple times
|
|
112
|
+
3. **Efficient updates**: Only changed objects trigger re-renders
|
|
113
|
+
|
|
114
|
+
## Type Policies
|
|
115
|
+
|
|
116
|
+
### keyFields
|
|
117
|
+
|
|
118
|
+
Customize how objects are identified in the cache.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const cache = new InMemoryCache({
|
|
122
|
+
typePolicies: {
|
|
123
|
+
// Use ISBN instead of id for books
|
|
124
|
+
Book: {
|
|
125
|
+
keyFields: ["isbn"],
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Composite key
|
|
129
|
+
UserSession: {
|
|
130
|
+
keyFields: ["userId", "deviceId"],
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Nested key
|
|
134
|
+
Review: {
|
|
135
|
+
keyFields: ["book", ["isbn"], "reviewer", ["id"]],
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// No key fields (singleton, only one object in cache per type)
|
|
139
|
+
AppSettings: {
|
|
140
|
+
keyFields: [],
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Disable normalization (objects of this type will be stored with their
|
|
144
|
+
// parent entity. The same object might end up multiple times in the cache
|
|
145
|
+
// and run out of sync. Use with caution, only if this object really relates
|
|
146
|
+
// to a property of their parent entity and cannot exist on its own.)
|
|
147
|
+
Address: {
|
|
148
|
+
keyFields: false,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### merge Functions
|
|
155
|
+
|
|
156
|
+
Control how incoming data merges with existing data.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const cache = new InMemoryCache({
|
|
160
|
+
typePolicies: {
|
|
161
|
+
User: {
|
|
162
|
+
fields: {
|
|
163
|
+
// Deep merge profile object
|
|
164
|
+
profile: {
|
|
165
|
+
merge: true, // Shorthand for deep merge
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Custom merge logic
|
|
169
|
+
notifications: {
|
|
170
|
+
merge(existing = [], incoming, { mergeObjects }) {
|
|
171
|
+
// Prepend new notifications
|
|
172
|
+
return [...incoming, ...existing];
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Field Policies
|
|
182
|
+
|
|
183
|
+
### read Function
|
|
184
|
+
|
|
185
|
+
Transform cached data when reading.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const cache = new InMemoryCache({
|
|
189
|
+
typePolicies: {
|
|
190
|
+
User: {
|
|
191
|
+
fields: {
|
|
192
|
+
// Computed field
|
|
193
|
+
fullName: {
|
|
194
|
+
read(_, { readField }) {
|
|
195
|
+
const firstName = readField("firstName");
|
|
196
|
+
const lastName = readField("lastName");
|
|
197
|
+
return `${firstName} ${lastName}`;
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Transform existing field
|
|
202
|
+
birthDate: {
|
|
203
|
+
read(existing) {
|
|
204
|
+
return existing ? new Date(existing) : null;
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// Default value
|
|
209
|
+
role: {
|
|
210
|
+
read(existing = "USER") {
|
|
211
|
+
return existing;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### merge Function
|
|
221
|
+
|
|
222
|
+
Control how incoming data is stored.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const cache = new InMemoryCache({
|
|
226
|
+
typePolicies: {
|
|
227
|
+
User: {
|
|
228
|
+
fields: {
|
|
229
|
+
// Accumulate items instead of replacing
|
|
230
|
+
friends: {
|
|
231
|
+
merge(existing = [], incoming) {
|
|
232
|
+
return [...existing, ...incoming];
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// Merge objects deeply
|
|
237
|
+
settings: {
|
|
238
|
+
merge(existing, incoming, { mergeObjects }) {
|
|
239
|
+
return mergeObjects(existing, incoming);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
Query: {
|
|
246
|
+
fields: {
|
|
247
|
+
// Merge paginated results
|
|
248
|
+
posts: {
|
|
249
|
+
keyArgs: ["category"], // Only category affects cache key
|
|
250
|
+
merge(existing = { items: [] }, incoming) {
|
|
251
|
+
return {
|
|
252
|
+
...incoming,
|
|
253
|
+
items: [...existing.items, ...incoming.items],
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### keyArgs
|
|
264
|
+
|
|
265
|
+
Control which arguments affect cache storage.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const cache = new InMemoryCache({
|
|
269
|
+
typePolicies: {
|
|
270
|
+
Query: {
|
|
271
|
+
fields: {
|
|
272
|
+
// Different cache entry per userId only
|
|
273
|
+
// (limit, offset don't create new entries)
|
|
274
|
+
userPosts: {
|
|
275
|
+
keyArgs: ["userId"],
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// No arguments affect cache key
|
|
279
|
+
// (useful for pagination)
|
|
280
|
+
feed: {
|
|
281
|
+
keyArgs: false,
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Nested argument
|
|
285
|
+
search: {
|
|
286
|
+
keyArgs: ["filter", ["category", "status"]],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Pagination
|
|
295
|
+
|
|
296
|
+
### Offset-Based Pagination
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { offsetLimitPagination } from "@apollo/client/utilities";
|
|
300
|
+
|
|
301
|
+
const cache = new InMemoryCache({
|
|
302
|
+
typePolicies: {
|
|
303
|
+
Query: {
|
|
304
|
+
fields: {
|
|
305
|
+
posts: offsetLimitPagination(),
|
|
306
|
+
|
|
307
|
+
// With key arguments
|
|
308
|
+
userPosts: offsetLimitPagination(["userId"]),
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Cursor-Based Pagination (Relay Style)
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { relayStylePagination } from "@apollo/client/utilities";
|
|
319
|
+
|
|
320
|
+
const cache = new InMemoryCache({
|
|
321
|
+
typePolicies: {
|
|
322
|
+
Query: {
|
|
323
|
+
fields: {
|
|
324
|
+
posts: relayStylePagination(),
|
|
325
|
+
|
|
326
|
+
// With key arguments
|
|
327
|
+
userPosts: relayStylePagination(["userId"]),
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Custom Pagination
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const cache = new InMemoryCache({
|
|
338
|
+
typePolicies: {
|
|
339
|
+
Query: {
|
|
340
|
+
fields: {
|
|
341
|
+
feed: {
|
|
342
|
+
keyArgs: false,
|
|
343
|
+
|
|
344
|
+
merge(existing, incoming, { args }) {
|
|
345
|
+
const merged = existing ? existing.slice(0) : [];
|
|
346
|
+
const offset = args?.offset ?? 0;
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < incoming.length; i++) {
|
|
349
|
+
merged[offset + i] = incoming[i];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return merged;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
read(existing, { args }) {
|
|
356
|
+
const offset = args?.offset ?? 0;
|
|
357
|
+
const limit = args?.limit ?? existing?.length ?? 0;
|
|
358
|
+
return existing?.slice(offset, offset + limit);
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### fetchMore for Pagination
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
function PostList() {
|
|
371
|
+
const { data, fetchMore, loading } = useQuery(GET_POSTS, {
|
|
372
|
+
variables: { offset: 0, limit: 10 },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const loadMore = () => {
|
|
376
|
+
fetchMore({
|
|
377
|
+
variables: {
|
|
378
|
+
offset: data.posts.length,
|
|
379
|
+
},
|
|
380
|
+
// With proper type policies, no updateQuery needed
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div>
|
|
386
|
+
{data?.posts.map((post) => <PostCard key={post.id} post={post} />)}
|
|
387
|
+
<button onClick={loadMore} disabled={loading}>
|
|
388
|
+
Load More
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Cache Manipulation
|
|
396
|
+
|
|
397
|
+
### cache.readQuery
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// Read data from cache
|
|
401
|
+
const data = cache.readQuery({
|
|
402
|
+
query: GET_TODOS,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// With variables
|
|
406
|
+
const userData = cache.readQuery({
|
|
407
|
+
query: GET_USER,
|
|
408
|
+
variables: { id: "1" },
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### cache.writeQuery
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// Write data to cache
|
|
416
|
+
cache.writeQuery({
|
|
417
|
+
query: GET_TODOS,
|
|
418
|
+
data: {
|
|
419
|
+
todos: [
|
|
420
|
+
{ __typename: "Todo", id: "1", text: "Buy milk", completed: false },
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// With variables
|
|
426
|
+
cache.writeQuery({
|
|
427
|
+
query: GET_USER,
|
|
428
|
+
variables: { id: "1" },
|
|
429
|
+
data: {
|
|
430
|
+
user: { __typename: "User", id: "1", name: "John" },
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### cache.readFragment / cache.writeFragment
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// Read a specific object - use cache.identify for safety
|
|
439
|
+
const user = cache.readFragment({
|
|
440
|
+
id: cache.identify({ __typename: "User", id: "1" }),
|
|
441
|
+
fragment: gql`
|
|
442
|
+
fragment UserFragment on User {
|
|
443
|
+
id
|
|
444
|
+
name
|
|
445
|
+
email
|
|
446
|
+
}
|
|
447
|
+
`,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Apollo Client 4.1+: Use 'from' parameter (recommended)
|
|
451
|
+
const user = cache.readFragment({
|
|
452
|
+
from: { __typename: "User", id: "1" },
|
|
453
|
+
fragment: gql`
|
|
454
|
+
fragment UserFragment on User {
|
|
455
|
+
id
|
|
456
|
+
name
|
|
457
|
+
email
|
|
458
|
+
}
|
|
459
|
+
`,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Update a specific object
|
|
463
|
+
cache.writeFragment({
|
|
464
|
+
id: cache.identify({ __typename: "User", id: "1" }),
|
|
465
|
+
fragment: gql`
|
|
466
|
+
fragment UpdateUser on User {
|
|
467
|
+
name
|
|
468
|
+
}
|
|
469
|
+
`,
|
|
470
|
+
data: {
|
|
471
|
+
name: "Jane",
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Apollo Client 4.1+: Use 'from' parameter (recommended)
|
|
476
|
+
cache.writeFragment({
|
|
477
|
+
from: { __typename: "User", id: "1" },
|
|
478
|
+
fragment: gql`
|
|
479
|
+
fragment UpdateUser on User {
|
|
480
|
+
name
|
|
481
|
+
}
|
|
482
|
+
`,
|
|
483
|
+
data: {
|
|
484
|
+
name: "Jane",
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### cache.modify
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// Modify fields directly
|
|
493
|
+
cache.modify({
|
|
494
|
+
id: cache.identify(user),
|
|
495
|
+
fields: {
|
|
496
|
+
// Set new value
|
|
497
|
+
name: () => "New Name",
|
|
498
|
+
|
|
499
|
+
// Transform existing value
|
|
500
|
+
postCount: (existing) => existing + 1,
|
|
501
|
+
|
|
502
|
+
// Delete field
|
|
503
|
+
temporaryField: (_, { DELETE }) => DELETE,
|
|
504
|
+
|
|
505
|
+
// Add to array
|
|
506
|
+
friends: (existing, { toReference }) => [
|
|
507
|
+
...existing,
|
|
508
|
+
toReference({ __typename: "User", id: "2" }),
|
|
509
|
+
],
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### cache.evict
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// Remove object from cache
|
|
518
|
+
cache.evict({ id: "User:1" });
|
|
519
|
+
|
|
520
|
+
// Remove specific field
|
|
521
|
+
cache.evict({ id: "User:1", fieldName: "friends" });
|
|
522
|
+
|
|
523
|
+
// Remove with broadcast (trigger re-renders)
|
|
524
|
+
cache.evict({ id: "User:1", broadcast: true });
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Garbage Collection
|
|
528
|
+
|
|
529
|
+
### Manual Garbage Collection
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// After evicting objects, clean up dangling references
|
|
533
|
+
cache.evict({ id: "User:1" });
|
|
534
|
+
cache.gc();
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Retaining Objects
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Prevent objects from being garbage collected
|
|
541
|
+
const release = cache.retain("User:1");
|
|
542
|
+
|
|
543
|
+
// Later, allow GC
|
|
544
|
+
release();
|
|
545
|
+
cache.gc();
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Inspecting Cache
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
// Get all cached data
|
|
552
|
+
const cacheContents = cache.extract();
|
|
553
|
+
|
|
554
|
+
// Restore cache state
|
|
555
|
+
cache.restore(previousCacheContents);
|
|
556
|
+
|
|
557
|
+
// Get identified object cache key
|
|
558
|
+
const userId = cache.identify({ __typename: "User", id: "1" });
|
|
559
|
+
// Returns: 'User:1'
|
|
560
|
+
```
|