@apollo/client 4.1.7 → 4.1.9
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 +13 -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 -7
- 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 +350 -0
- package/skills/apollo-client/references/fragments.md +804 -0
- package/skills/apollo-client/references/integration-client.md +336 -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,804 @@
|
|
|
1
|
+
# Fragments Reference
|
|
2
|
+
|
|
3
|
+
GraphQL fragments define a set of fields for a specific type. In Apollo Client, fragments are especially powerful when colocated with components to define each component's data requirements independently, creating a clear separation of concerns and enabling better component composition.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [What Are Fragments](#what-are-fragments)
|
|
8
|
+
- [Basic Fragment Syntax](#basic-fragment-syntax)
|
|
9
|
+
- [Fragment Colocation](#fragment-colocation)
|
|
10
|
+
- [Fragment Reading Hooks](#fragment-reading-hooks)
|
|
11
|
+
- [Data Masking](#data-masking)
|
|
12
|
+
- [Fragment Registry](#fragment-registry)
|
|
13
|
+
- [TypeScript Integration](#typescript-integration)
|
|
14
|
+
- [Best Practices](#best-practices)
|
|
15
|
+
|
|
16
|
+
## What Are Fragments
|
|
17
|
+
|
|
18
|
+
A GraphQL fragment defines a set of fields for a specific GraphQL type. Fragments are defined on a specific GraphQL type and can be included in operations using the spread operator (`...`).
|
|
19
|
+
|
|
20
|
+
In Apollo Client, fragments serve a specific purpose:
|
|
21
|
+
|
|
22
|
+
**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment. This allows components to independently evolve their data requirements without creating artificial dependencies between unrelated parts of your application.
|
|
23
|
+
|
|
24
|
+
Fragments enable:
|
|
25
|
+
|
|
26
|
+
1. **Component colocation**: Define the exact data requirements for a component alongside the component code
|
|
27
|
+
2. **Independent evolution**: Change a component's data needs without affecting other components
|
|
28
|
+
3. **Code organization**: Compose fragments together to build complete queries that mirror your component hierarchy
|
|
29
|
+
|
|
30
|
+
## Basic Fragment Syntax
|
|
31
|
+
|
|
32
|
+
### Defining a Fragment
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { gql } from "@apollo/client";
|
|
36
|
+
|
|
37
|
+
const USER_FRAGMENT = gql`
|
|
38
|
+
fragment UserFields on User {
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
email
|
|
42
|
+
avatarUrl
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every fragment includes:
|
|
48
|
+
|
|
49
|
+
- A unique name (`UserFields`)
|
|
50
|
+
- The type it operates on (`User`)
|
|
51
|
+
- The fields to select
|
|
52
|
+
|
|
53
|
+
### Using Fragments in Queries
|
|
54
|
+
|
|
55
|
+
Include fragments in queries using the spread operator:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const GET_USER = gql`
|
|
59
|
+
query GetUser($id: ID!) {
|
|
60
|
+
user(id: $id) {
|
|
61
|
+
...UserFields
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
${USER_FRAGMENT}
|
|
66
|
+
`;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When using GraphQL Code Generator with the recommended configuration (typescript, typescript-operations, and typed-document-node plugins), fragments defined in your source files are automatically picked up and generated into typed document nodes. The generated fragment documents already include the fragment definition, so you don't need to interpolate them manually into queries.
|
|
70
|
+
|
|
71
|
+
## Fragment Colocation
|
|
72
|
+
|
|
73
|
+
Fragment colocation is the practice of defining fragments in the same file as the component that uses them. This creates a clear contract between components and their data requirements.
|
|
74
|
+
|
|
75
|
+
### Why Colocate Fragments
|
|
76
|
+
|
|
77
|
+
- **Locality**: Data requirements live next to the code that uses them
|
|
78
|
+
- **Maintainability**: Changes to component UI and data needs happen together
|
|
79
|
+
- **Type safety**: TypeScript can infer exact types from colocated fragments
|
|
80
|
+
- **Independence**: Components can evolve their data requirements without affecting other components
|
|
81
|
+
|
|
82
|
+
### Colocation Pattern
|
|
83
|
+
|
|
84
|
+
The recommended pattern for colocating fragments with components:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { gql, FragmentType } from "@apollo/client";
|
|
88
|
+
import { useSuspenseFragment } from "@apollo/client/react";
|
|
89
|
+
|
|
90
|
+
// Fragment definition
|
|
91
|
+
// This will be picked up by Codegen to create `UserCard_UserFragmentDoc` in `./fragments.generated.ts`.
|
|
92
|
+
// As that generated fragment document is correctly typed, we use that in the code going forward.
|
|
93
|
+
// This fragment will never be consumed in runtime code, so it is wrapped in `if (false)` so the bundler can omit it when bundling.
|
|
94
|
+
if (false) {
|
|
95
|
+
gql`
|
|
96
|
+
fragment UserCard_user on User {
|
|
97
|
+
id
|
|
98
|
+
name
|
|
99
|
+
email
|
|
100
|
+
avatarUrl
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// This has been created from above fragment definition by CodeGen and is a correctly typed `TypedDocumentNode`
|
|
106
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated.ts";
|
|
107
|
+
|
|
108
|
+
// Component receives the (partially masked) parent object
|
|
109
|
+
export function UserCard({
|
|
110
|
+
user,
|
|
111
|
+
}: {
|
|
112
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
113
|
+
}) {
|
|
114
|
+
// Creates a subscription to the fragment in the cache
|
|
115
|
+
const { data } = useSuspenseFragment({
|
|
116
|
+
fragment: UserCard_UserFragmentDoc,
|
|
117
|
+
fragmentName: "UserCard_user",
|
|
118
|
+
from: user,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<img src={data.avatarUrl} alt={data.name} />
|
|
124
|
+
<h2>{data.name}</h2>
|
|
125
|
+
<p>{data.email}</p>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Naming Convention
|
|
132
|
+
|
|
133
|
+
A suggested naming pattern for fragments follows this convention:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
{ComponentName}_{propName}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Where `propName` is the name of the prop the component receives containing the fragment data.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
|
|
143
|
+
- `UserCard_user` - Fragment for the `user` prop in the UserCard component
|
|
144
|
+
- `PostList_posts` - Fragment for the `posts` prop in the PostList component
|
|
145
|
+
- `CommentItem_comment` - Fragment for the `comment` prop in the CommentItem component
|
|
146
|
+
|
|
147
|
+
This convention makes it clear which component owns which fragment. However, you can choose a different naming convention based on your project's needs.
|
|
148
|
+
|
|
149
|
+
**Note**: A component might accept fragment data through multiple props, in which case it would have multiple associated fragments. For example, a `CommentCard` component might accept both a `comment` prop and an `author` prop, resulting in `CommentCard_comment` and `CommentCard_author` fragments.
|
|
150
|
+
|
|
151
|
+
### Composing Fragments
|
|
152
|
+
|
|
153
|
+
Parent components compose child fragments to build complete queries:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// Child component
|
|
157
|
+
import { gql } from "@apollo/client";
|
|
158
|
+
|
|
159
|
+
if (false) {
|
|
160
|
+
gql`
|
|
161
|
+
fragment UserAvatar_user on User {
|
|
162
|
+
id
|
|
163
|
+
avatarUrl
|
|
164
|
+
name
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Parent component composes child fragments
|
|
170
|
+
if (false) {
|
|
171
|
+
gql`
|
|
172
|
+
fragment UserProfile_user on User {
|
|
173
|
+
id
|
|
174
|
+
name
|
|
175
|
+
email
|
|
176
|
+
bio
|
|
177
|
+
...UserAvatar_user
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Page-level query composes all fragments
|
|
183
|
+
if (false) {
|
|
184
|
+
gql`
|
|
185
|
+
query UserProfilePage($id: ID!) {
|
|
186
|
+
user(id: $id) {
|
|
187
|
+
...UserProfile_user
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This creates a hierarchy that mirrors your component tree.
|
|
195
|
+
|
|
196
|
+
## Fragment Reading Hooks
|
|
197
|
+
|
|
198
|
+
Apollo Client provides hooks to read fragment data within components. These hooks work with data masking to ensure components only access the data they explicitly requested.
|
|
199
|
+
|
|
200
|
+
### useSuspenseFragment
|
|
201
|
+
|
|
202
|
+
For components using Suspense and concurrent features:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { useSuspenseFragment } from "@apollo/client/react";
|
|
206
|
+
import { FragmentType } from "@apollo/client";
|
|
207
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
208
|
+
|
|
209
|
+
function UserCard({
|
|
210
|
+
user,
|
|
211
|
+
}: {
|
|
212
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
213
|
+
}) {
|
|
214
|
+
const { data } = useSuspenseFragment({
|
|
215
|
+
fragment: UserCard_UserFragmentDoc,
|
|
216
|
+
fragmentName: "UserCard_user",
|
|
217
|
+
from: user,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return <div>{data.name}</div>;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### useFragment
|
|
225
|
+
|
|
226
|
+
For components not using Suspense:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { useFragment } from "@apollo/client/react";
|
|
230
|
+
import { FragmentType } from "@apollo/client";
|
|
231
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
232
|
+
|
|
233
|
+
function UserCard({
|
|
234
|
+
user,
|
|
235
|
+
}: {
|
|
236
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
237
|
+
}) {
|
|
238
|
+
const { data, complete } = useFragment({
|
|
239
|
+
fragment: UserCard_UserFragmentDoc,
|
|
240
|
+
fragmentName: "UserCard_user",
|
|
241
|
+
from: user,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!complete) {
|
|
245
|
+
return <div>Loading...</div>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return <div>{data.name}</div>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The `complete` field indicates whether all fragment data is available in the cache.
|
|
253
|
+
|
|
254
|
+
### Hook Options
|
|
255
|
+
|
|
256
|
+
Both hooks accept these options:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
{
|
|
260
|
+
// The fragment document (required)
|
|
261
|
+
fragment: TypedDocumentNode,
|
|
262
|
+
|
|
263
|
+
// The fragment name (optional in most cases)
|
|
264
|
+
// Only required if the fragment document contains multiple definitions
|
|
265
|
+
fragmentName?: string,
|
|
266
|
+
|
|
267
|
+
// The source data containing the fragment (required)
|
|
268
|
+
// Can be a single object or an array of objects
|
|
269
|
+
from: FragmentType<typeof fragment> | Array<FragmentType<typeof fragment>>,
|
|
270
|
+
|
|
271
|
+
// Variables for the fragment (optional)
|
|
272
|
+
variables?: Variables,
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
When `from` is an array, the hook returns an array of results, allowing you to read fragments from multiple objects efficiently. **Note**: Array support for the `from` parameter was added in Apollo Client 4.1.0.
|
|
277
|
+
|
|
278
|
+
## Data Masking
|
|
279
|
+
|
|
280
|
+
Data masking is a feature that prevents components from accessing data they didn't explicitly request through their fragments. This enforces proper data boundaries and prevents over-rendering.
|
|
281
|
+
|
|
282
|
+
### Enabling Data Masking
|
|
283
|
+
|
|
284
|
+
Enable data masking when creating your Apollo Client:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
|
288
|
+
|
|
289
|
+
const client = new ApolloClient({
|
|
290
|
+
cache: new InMemoryCache(),
|
|
291
|
+
dataMasking: true, // Enable data masking
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### How Data Masking Works
|
|
296
|
+
|
|
297
|
+
With data masking enabled:
|
|
298
|
+
|
|
299
|
+
1. Fragments return opaque `FragmentType` objects
|
|
300
|
+
2. Components must use `useFragment` or `useSuspenseFragment` to unmask data
|
|
301
|
+
3. Components can only access fields defined in their own fragments
|
|
302
|
+
4. TypeScript enforces these boundaries at compile time
|
|
303
|
+
|
|
304
|
+
Without data masking:
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
// ❌ Without data masking - component can access any data from parent
|
|
308
|
+
function UserCard({ user }: { user: User }) {
|
|
309
|
+
// Can access any User field, even if not in fragment
|
|
310
|
+
return <div>{user.privateData}</div>;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
With data masking:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
// ✅ With data masking - component can only access its fragment data
|
|
318
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
319
|
+
|
|
320
|
+
function UserCard({
|
|
321
|
+
user,
|
|
322
|
+
}: {
|
|
323
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
324
|
+
}) {
|
|
325
|
+
const { data } = useSuspenseFragment({
|
|
326
|
+
fragment: UserCard_UserFragmentDoc,
|
|
327
|
+
from: user,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// TypeScript error: 'privateData' doesn't exist on fragment type
|
|
331
|
+
// return <div>{data.privateData}</div>;
|
|
332
|
+
|
|
333
|
+
// Only fields from the fragment are accessible
|
|
334
|
+
return <div>{data.name}</div>;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Benefits of Data Masking
|
|
339
|
+
|
|
340
|
+
- **Prevents over-rendering**: Components only re-render when their specific data changes
|
|
341
|
+
- **Enforces boundaries**: Components can't accidentally depend on data they don't own
|
|
342
|
+
- **Better refactoring**: Safe to modify parent queries without breaking child components
|
|
343
|
+
- **Type safety**: TypeScript catches attempts to access unavailable fields
|
|
344
|
+
|
|
345
|
+
## Fragment Registry
|
|
346
|
+
|
|
347
|
+
The fragment registry is an **alternative approach** to GraphQL Code Generator's automatic fragment inlining by name. It allows you to register fragments globally, making them available throughout your application by name reference.
|
|
348
|
+
|
|
349
|
+
**Important**: GraphQL Code Generator automatically inlines fragments by name wherever they're used in your queries. Either approach is sufficient on its own—**you don't need to combine them**.
|
|
350
|
+
|
|
351
|
+
### Creating a Fragment Registry
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
|
355
|
+
import { createFragmentRegistry } from "@apollo/client/cache";
|
|
356
|
+
|
|
357
|
+
export const fragmentRegistry = createFragmentRegistry();
|
|
358
|
+
|
|
359
|
+
const client = new ApolloClient({
|
|
360
|
+
cache: new InMemoryCache({
|
|
361
|
+
fragments: fragmentRegistry,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Registering Fragments
|
|
367
|
+
|
|
368
|
+
Register fragments after defining them:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { gql } from "@apollo/client";
|
|
372
|
+
import { fragmentRegistry } from "./apollo/client";
|
|
373
|
+
|
|
374
|
+
const USER_FRAGMENT = gql`
|
|
375
|
+
fragment UserFields on User {
|
|
376
|
+
id
|
|
377
|
+
name
|
|
378
|
+
email
|
|
379
|
+
}
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
fragmentRegistry.register(USER_FRAGMENT);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
With colocated fragments:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { fragmentRegistry } from "@/apollo/client";
|
|
389
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
390
|
+
|
|
391
|
+
// Register the fragment globally
|
|
392
|
+
fragmentRegistry.register(UserCard_UserFragmentDoc);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Using Registered Fragments
|
|
396
|
+
|
|
397
|
+
Once registered, fragments can be referenced by name in queries without explicit imports:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// Fragment is available by name because it's registered
|
|
401
|
+
const GET_USER = gql`
|
|
402
|
+
query GetUser($id: ID!) {
|
|
403
|
+
user(id: $id) {
|
|
404
|
+
...UserCard_user
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Approaches for Fragment Composition
|
|
411
|
+
|
|
412
|
+
There are three approaches to make child fragments available in parent queries:
|
|
413
|
+
|
|
414
|
+
1. **GraphQL Code Generator inlining** (Recommended): CodeGen automatically inlines fragments by name. No manual work needed—just reference fragments by name in your queries.
|
|
415
|
+
|
|
416
|
+
2. **Fragment Registry**: Manually register fragments to make them available by name. Useful for runtime scenarios where CodeGen isn't available.
|
|
417
|
+
|
|
418
|
+
3. **Manual interpolation**: Explicitly import and interpolate child fragments into parent fragments:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { CHILD_FRAGMENT } from "./ChildComponent";
|
|
422
|
+
|
|
423
|
+
const PARENT_FRAGMENT = gql`
|
|
424
|
+
fragment Parent_data on Data {
|
|
425
|
+
field
|
|
426
|
+
...Child_data
|
|
427
|
+
}
|
|
428
|
+
${CHILD_FRAGMENT}
|
|
429
|
+
`;
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Pros and Cons
|
|
433
|
+
|
|
434
|
+
**GraphQL Code Generator inlining**:
|
|
435
|
+
|
|
436
|
+
- ✅ Less work: Automatic, no manual registration needed
|
|
437
|
+
- ❌ Larger bundle: Fragments are inlined into every query that uses them
|
|
438
|
+
|
|
439
|
+
**Fragment Registry**:
|
|
440
|
+
|
|
441
|
+
- ✅ Smaller bundle: Fragments are registered once, referenced by name
|
|
442
|
+
- ❌ More work: Requires manual registration of each fragment
|
|
443
|
+
- ❌ May cause issues with lazy-loaded modules if the module is not loaded before the query is executed
|
|
444
|
+
- ✅ Best for deeply nested component trees where bundle size matters
|
|
445
|
+
|
|
446
|
+
**Manual interpolation**:
|
|
447
|
+
|
|
448
|
+
- ❌ Most work: Manual imports and interpolation required
|
|
449
|
+
- ✅ Explicit: Clear fragment dependencies in code
|
|
450
|
+
|
|
451
|
+
### Recommendation
|
|
452
|
+
|
|
453
|
+
For most applications using GraphQL Code Generator (as shown in this guide), **use the automatic inlining**—it requires no additional setup and works seamlessly. Consider the fragment registry only if bundle size becomes a concern in applications with deeply nested component trees.
|
|
454
|
+
|
|
455
|
+
## TypeScript Integration
|
|
456
|
+
|
|
457
|
+
Apollo Client provides strong TypeScript support for fragments through GraphQL Code Generator.
|
|
458
|
+
|
|
459
|
+
### Generated Types
|
|
460
|
+
|
|
461
|
+
GraphQL Code Generator produces typed fragment documents:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// Generated file: fragments.generated.ts
|
|
465
|
+
export type UserCard_UserFragment = {
|
|
466
|
+
__typename: "User";
|
|
467
|
+
id: string;
|
|
468
|
+
name: string;
|
|
469
|
+
email: string;
|
|
470
|
+
avatarUrl: string;
|
|
471
|
+
} & { " $fragmentName"?: "UserCard_UserFragment" };
|
|
472
|
+
|
|
473
|
+
export const UserCard_UserFragmentDoc: TypedDocumentNode<
|
|
474
|
+
UserCard_UserFragment,
|
|
475
|
+
never
|
|
476
|
+
>;
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Type-Safe Fragment Usage
|
|
480
|
+
|
|
481
|
+
Use `FragmentType` to accept masked fragment data:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
import { FragmentType } from "@apollo/client";
|
|
485
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
486
|
+
|
|
487
|
+
function UserCard({
|
|
488
|
+
user,
|
|
489
|
+
}: {
|
|
490
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
491
|
+
}) {
|
|
492
|
+
const { data } = useSuspenseFragment({
|
|
493
|
+
fragment: UserCard_UserFragmentDoc,
|
|
494
|
+
from: user,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// 'data' is fully typed as UserCard_UserFragment
|
|
498
|
+
return <div>{data.name}</div>;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Fragment Type Inference
|
|
503
|
+
|
|
504
|
+
TypeScript infers types from fragment documents automatically:
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
508
|
+
|
|
509
|
+
// Types are inferred from the fragment
|
|
510
|
+
const { data } = useSuspenseFragment({
|
|
511
|
+
fragment: UserCard_UserFragmentDoc,
|
|
512
|
+
from: user,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// data.name is string
|
|
516
|
+
// data.email is string
|
|
517
|
+
// data.nonExistentField is a TypeScript error
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Parent-Child Type Safety
|
|
521
|
+
|
|
522
|
+
When passing fragment data from parent to child:
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
// Parent query
|
|
526
|
+
const { data } = useSuspenseQuery(GET_USER);
|
|
527
|
+
|
|
528
|
+
// TypeScript ensures the query includes UserCard_user fragment
|
|
529
|
+
// before allowing it to be passed to UserCard
|
|
530
|
+
<UserCard user={data.user} />;
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Best Practices
|
|
534
|
+
|
|
535
|
+
### Prefer Colocation Over Reuse
|
|
536
|
+
|
|
537
|
+
**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment, even if multiple components currently need the same fields.
|
|
538
|
+
|
|
539
|
+
Sharing fragments between components just because they happen to need the same fields today creates artificial dependencies. When one component's requirements change, the shared fragment must be updated, causing all components using it to over-fetch data they don't need.
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
// ✅ Good: Each component has its own fragment
|
|
543
|
+
if (false) {
|
|
544
|
+
gql`
|
|
545
|
+
fragment UserCard_user on User {
|
|
546
|
+
id
|
|
547
|
+
name
|
|
548
|
+
email
|
|
549
|
+
avatarUrl
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
gql`
|
|
554
|
+
fragment UserListItem_user on User {
|
|
555
|
+
id
|
|
556
|
+
name
|
|
557
|
+
email
|
|
558
|
+
}
|
|
559
|
+
`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If UserCard later needs 'bio', only UserCard_user changes
|
|
563
|
+
// UserListItem doesn't over-fetch 'bio'
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
// ❌ Avoid: Sharing a generic fragment across components
|
|
568
|
+
const COMMON_USER_FIELDS = gql`
|
|
569
|
+
fragment CommonUserFields on User {
|
|
570
|
+
id
|
|
571
|
+
name
|
|
572
|
+
email
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
|
|
576
|
+
// UserCard and UserListItem both use CommonUserFields
|
|
577
|
+
// When UserCard needs 'bio', adding it to CommonUserFields
|
|
578
|
+
// causes UserListItem to over-fetch unnecessarily
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
This independence allows each component to evolve its data requirements without affecting unrelated parts of your application.
|
|
582
|
+
|
|
583
|
+
### One Query Per Page
|
|
584
|
+
|
|
585
|
+
Compose all page data requirements into a single query at the page level:
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
// ✅ Good: Single page-level query
|
|
589
|
+
if (false) {
|
|
590
|
+
gql`
|
|
591
|
+
query UserProfilePage($id: ID!) {
|
|
592
|
+
user(id: $id) {
|
|
593
|
+
...UserHeader_user
|
|
594
|
+
...UserPosts_user
|
|
595
|
+
...UserFriends_user
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
// ❌ Avoid: Multiple queries in different components
|
|
604
|
+
function UserProfile() {
|
|
605
|
+
const { data: userData } = useQuery(GET_USER);
|
|
606
|
+
const { data: postsData } = useQuery(GET_USER_POSTS);
|
|
607
|
+
const { data: friendsData } = useQuery(GET_USER_FRIENDS);
|
|
608
|
+
// ...
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Use Fragment-Reading Hooks in Components
|
|
613
|
+
|
|
614
|
+
Non-page components should use `useFragment` or `useSuspenseFragment`:
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
// ✅ Good: Component reads fragment data
|
|
618
|
+
import { FragmentType } from "@apollo/client";
|
|
619
|
+
import { useSuspenseFragment } from "@apollo/client/react";
|
|
620
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
621
|
+
|
|
622
|
+
function UserCard({
|
|
623
|
+
user,
|
|
624
|
+
}: {
|
|
625
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
626
|
+
}) {
|
|
627
|
+
const { data } = useSuspenseFragment({
|
|
628
|
+
fragment: UserCard_UserFragmentDoc,
|
|
629
|
+
from: user,
|
|
630
|
+
});
|
|
631
|
+
return <div>{data.name}</div>;
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
// ❌ Avoid: Component uses query hook
|
|
637
|
+
function UserCard({ userId }: { userId: string }) {
|
|
638
|
+
const { data } = useQuery(GET_USER, { variables: { id: userId } });
|
|
639
|
+
return <div>{data.user.name}</div>;
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Request Only Required Fields
|
|
644
|
+
|
|
645
|
+
Keep fragments minimal and only request fields the component actually uses:
|
|
646
|
+
|
|
647
|
+
```tsx
|
|
648
|
+
// ✅ Good: Only necessary fields
|
|
649
|
+
if (false) {
|
|
650
|
+
gql`
|
|
651
|
+
fragment UserListItem_user on User {
|
|
652
|
+
id
|
|
653
|
+
name
|
|
654
|
+
}
|
|
655
|
+
`;
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
```tsx
|
|
660
|
+
// ❌ Avoid: Requesting unused fields
|
|
661
|
+
if (false) {
|
|
662
|
+
gql`
|
|
663
|
+
fragment UserListItem_user on User {
|
|
664
|
+
id
|
|
665
|
+
name
|
|
666
|
+
email
|
|
667
|
+
bio
|
|
668
|
+
friends {
|
|
669
|
+
id
|
|
670
|
+
name
|
|
671
|
+
}
|
|
672
|
+
posts {
|
|
673
|
+
id
|
|
674
|
+
title
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Use @defer for Below-the-Fold Content
|
|
682
|
+
|
|
683
|
+
The `@defer` directive allows you to defer loading of non-critical fields, enabling faster initial page loads by prioritizing essential data. The deferred fields are delivered via incremental delivery and arrive after the non-deferred data, allowing the UI to progressively render as data becomes available.
|
|
684
|
+
|
|
685
|
+
Defer slow fields that aren't immediately visible:
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
if (false) {
|
|
689
|
+
gql`
|
|
690
|
+
query ProductPage($id: ID!) {
|
|
691
|
+
product(id: $id) {
|
|
692
|
+
id
|
|
693
|
+
name
|
|
694
|
+
price
|
|
695
|
+
...ProductReviews_product @defer
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
This allows the page to render quickly while reviews load in the background.
|
|
703
|
+
|
|
704
|
+
### Handle Client-Only Fields
|
|
705
|
+
|
|
706
|
+
Use the `@client` directive for fields resolved locally:
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
if (false) {
|
|
710
|
+
gql`
|
|
711
|
+
fragment TodoItem_todo on Todo {
|
|
712
|
+
id
|
|
713
|
+
text
|
|
714
|
+
completed
|
|
715
|
+
isSelected @client
|
|
716
|
+
}
|
|
717
|
+
`;
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Enable Data Masking for New Applications
|
|
722
|
+
|
|
723
|
+
Always enable data masking in new applications:
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
const client = new ApolloClient({
|
|
727
|
+
cache: new InMemoryCache(),
|
|
728
|
+
dataMasking: true,
|
|
729
|
+
});
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
This enforces proper boundaries from the start and prevents accidental coupling between components.
|
|
733
|
+
|
|
734
|
+
## Apollo Client Data Masking vs GraphQL-Codegen Fragment Masking
|
|
735
|
+
|
|
736
|
+
Apollo Client's data masking and GraphQL Code Generator's fragment masking are different features that serve different purposes:
|
|
737
|
+
|
|
738
|
+
### GraphQL-Codegen Fragment Masking
|
|
739
|
+
|
|
740
|
+
GraphQL Code Generator's fragment masking (when using the client preset) is a **type-level** feature:
|
|
741
|
+
|
|
742
|
+
- Masks data only at the TypeScript type level
|
|
743
|
+
- The actual runtime data remains fully accessible on the object
|
|
744
|
+
- Using their `useFragment` hook simply "unmasks" the data on a type level
|
|
745
|
+
- Does not prevent accidental access to data at runtime
|
|
746
|
+
- Parent components receive all data and pass it down
|
|
747
|
+
- This means the parent component has to be subscribed to all data
|
|
748
|
+
|
|
749
|
+
### Apollo Client Data Masking
|
|
750
|
+
|
|
751
|
+
Apollo Client's data masking is a **runtime** feature with significant performance benefits:
|
|
752
|
+
|
|
753
|
+
- Removes data at the runtime level, not just the type level
|
|
754
|
+
- The `useFragment` and `useSuspenseFragment` hooks create cache subscriptions
|
|
755
|
+
- Parent objects are sparse and only contain unmasked data
|
|
756
|
+
- Prevents accidental access to data that should be masked
|
|
757
|
+
|
|
758
|
+
### Key Benefits of Apollo Client Data Masking
|
|
759
|
+
|
|
760
|
+
**1. No Accidental Data Access**
|
|
761
|
+
|
|
762
|
+
With runtime data masking, masked fields are not present in the parent object at all. You cannot accidentally access them, even if you bypass TypeScript type checking.
|
|
763
|
+
|
|
764
|
+
**2. Fewer Re-renders**
|
|
765
|
+
|
|
766
|
+
Apollo Client's approach creates more efficient subscriptions:
|
|
767
|
+
|
|
768
|
+
- **Without data masking**: Parent component subscribes to all fields (including masked ones). When a masked child field changes, the parent re-renders to pass that runtime data down the tree.
|
|
769
|
+
- **With data masking**: Parent component only subscribes to its own unmasked fields. Subscriptions on masked fields happen lower in the React component tree when the child component calls `useSuspenseFragment`. When a masked field changes, only the child component that subscribed to it re-renders.
|
|
770
|
+
|
|
771
|
+
### Example
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
import { FragmentType } from "@apollo/client";
|
|
775
|
+
import { useSuspenseQuery, useSuspenseFragment } from "@apollo/client/react";
|
|
776
|
+
import { UserCard_UserFragmentDoc } from "./fragments.generated";
|
|
777
|
+
|
|
778
|
+
function ParentComponent() {
|
|
779
|
+
const { data } = useSuspenseQuery(GET_USER);
|
|
780
|
+
|
|
781
|
+
// With Apollo Client data masking:
|
|
782
|
+
// - data.user only contains unmasked fields
|
|
783
|
+
// - Parent doesn't re-render when child-specific fields change
|
|
784
|
+
|
|
785
|
+
return <UserCard user={data.user} />;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function UserCard({
|
|
789
|
+
user,
|
|
790
|
+
}: {
|
|
791
|
+
user: FragmentType<typeof UserCard_UserFragmentDoc>;
|
|
792
|
+
}) {
|
|
793
|
+
// Creates a cache subscription specifically for UserCard_user fields
|
|
794
|
+
const { data } = useSuspenseFragment({
|
|
795
|
+
fragment: UserCard_UserFragmentDoc,
|
|
796
|
+
from: user,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Only this component re-renders when these fields change
|
|
800
|
+
return <div>{data.name}</div>;
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
This granular subscription approach improves performance in large applications with deeply nested component trees.
|