@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/__cjs/react/hooks/useSubscription.cjs +1 -1
  3. package/__cjs/react/hooks/useSubscription.cjs.map +1 -1
  4. package/__cjs/react/hooks/useSubscription.d.cts +2 -2
  5. package/__cjs/version.cjs +1 -1
  6. package/package.json +3 -7
  7. package/react/hooks/useSubscription.d.ts +2 -2
  8. package/react/hooks/useSubscription.js +1 -1
  9. package/react/hooks/useSubscription.js.map +1 -1
  10. package/react/hooks-compiled/useSubscription.d.ts +2 -2
  11. package/react/hooks-compiled/useSubscription.js +1 -1
  12. package/react/hooks-compiled/useSubscription.js.map +1 -1
  13. package/skills/apollo-client/SKILL.md +168 -0
  14. package/skills/apollo-client/references/caching.md +560 -0
  15. package/skills/apollo-client/references/error-handling.md +350 -0
  16. package/skills/apollo-client/references/fragments.md +804 -0
  17. package/skills/apollo-client/references/integration-client.md +336 -0
  18. package/skills/apollo-client/references/integration-nextjs.md +325 -0
  19. package/skills/apollo-client/references/integration-react-router.md +256 -0
  20. package/skills/apollo-client/references/integration-tanstack-start.md +378 -0
  21. package/skills/apollo-client/references/mutations.md +549 -0
  22. package/skills/apollo-client/references/queries.md +416 -0
  23. package/skills/apollo-client/references/state-management.md +428 -0
  24. package/skills/apollo-client/references/suspense-hooks.md +773 -0
  25. package/skills/apollo-client/references/troubleshooting.md +487 -0
  26. package/skills/apollo-client/references/typescript-codegen.md +133 -0
  27. package/version.js +1 -1
@@ -0,0 +1,549 @@
1
+ # Mutations Reference
2
+
3
+ ## Table of Contents
4
+
5
+ - [useMutation Hook](#usemutation-hook)
6
+ - [Mutation Variables](#mutation-variables)
7
+ - [Loading and Error States](#loading-and-error-states)
8
+ - [Optimistic UI](#optimistic-ui)
9
+ - [Cache Updates](#cache-updates)
10
+ - [Refetch Queries](#refetch-queries)
11
+ - [Error Handling](#error-handling)
12
+
13
+ ## useMutation Hook
14
+
15
+ The `useMutation` hook is used to execute GraphQL mutations.
16
+
17
+ ### Basic Usage
18
+
19
+ ```tsx
20
+ import { gql } from "@apollo/client";
21
+ import { useMutation } from "@apollo/client/react";
22
+
23
+ const ADD_TODO = gql`
24
+ mutation AddTodo($text: String!) {
25
+ addTodo(text: $text) {
26
+ id
27
+ text
28
+ completed
29
+ }
30
+ }
31
+ `;
32
+
33
+ function AddTodo() {
34
+ const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);
35
+
36
+ return (
37
+ <form
38
+ onSubmit={(e) => {
39
+ e.preventDefault();
40
+ const form = e.currentTarget;
41
+ const text = new FormData(form).get("text") as string;
42
+ addTodo({ variables: { text } });
43
+ form.reset();
44
+ }}
45
+ >
46
+ <input name="text" placeholder="Add todo" />
47
+ <button type="submit" disabled={loading}>
48
+ Add
49
+ </button>
50
+ {error && <p>Error: {error.message}</p>}
51
+ </form>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ### Return Tuple
57
+
58
+ ```typescript
59
+ const [
60
+ mutateFunction, // Function to call to execute mutation
61
+ {
62
+ data, // Mutation result data
63
+ loading, // True while mutation is in flight
64
+ error, // ApolloError if mutation failed
65
+ called, // True if mutation has been called
66
+ reset, // Reset mutation state
67
+ client, // Apollo Client instance
68
+ },
69
+ ] = useMutation(MUTATION);
70
+ ```
71
+
72
+ ## Mutation Variables
73
+
74
+ ### Variables in Options
75
+
76
+ ```tsx
77
+ const [createUser] = useMutation(CREATE_USER, {
78
+ variables: {
79
+ input: {
80
+ name: "Default User",
81
+ email: "default@example.com",
82
+ },
83
+ },
84
+ });
85
+
86
+ // Call with default variables
87
+ await createUser();
88
+
89
+ // Override variables
90
+ await createUser({
91
+ variables: {
92
+ input: {
93
+ name: "Custom User",
94
+ email: "custom@example.com",
95
+ },
96
+ },
97
+ });
98
+ ```
99
+
100
+ ### TypeScript Types
101
+
102
+ Use `TypedDocumentNode` instead of generic type parameters:
103
+
104
+ ```typescript
105
+ import { gql, TypedDocumentNode } from "@apollo/client";
106
+
107
+ interface CreateUserData {
108
+ createUser: {
109
+ id: string;
110
+ name: string;
111
+ email: string;
112
+ };
113
+ }
114
+
115
+ interface CreateUserVariables {
116
+ input: {
117
+ name: string;
118
+ email: string;
119
+ };
120
+ }
121
+
122
+ const CREATE_USER: TypedDocumentNode<CreateUserData, CreateUserVariables> = gql`
123
+ mutation CreateUser($input: CreateUserInput!) {
124
+ createUser(input: $input) {
125
+ id
126
+ name
127
+ email
128
+ }
129
+ }
130
+ `;
131
+
132
+ const [createUser, { data, loading }] = useMutation(CREATE_USER);
133
+
134
+ const { data } = await createUser({
135
+ variables: {
136
+ input: { name: "John", email: "john@example.com" },
137
+ },
138
+ });
139
+
140
+ // data.createUser is fully typed
141
+ ```
142
+
143
+ ## Loading and Error States
144
+
145
+ ### Handling in UI
146
+
147
+ ```tsx
148
+ function CreatePost() {
149
+ const [createPost, { loading, error, data, reset }] =
150
+ useMutation(CREATE_POST);
151
+
152
+ if (data) {
153
+ return (
154
+ <div>
155
+ <p>Post created: {data.createPost.title}</p>
156
+ <button onClick={reset}>Create another</button>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ return (
162
+ <form onSubmit={handleSubmit}>
163
+ <input name="title" disabled={loading} />
164
+ <textarea name="content" disabled={loading} />
165
+ <button type="submit" disabled={loading}>
166
+ {loading ? "Creating..." : "Create Post"}
167
+ </button>
168
+ {error && (
169
+ <div className="error">
170
+ <p>Failed to create post: {error.message}</p>
171
+ <button onClick={reset}>Try again</button>
172
+ </div>
173
+ )}
174
+ </form>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ### Async/Await Pattern
180
+
181
+ If you only need the promise without using the hook's loading/data state, use `client.mutate` instead:
182
+
183
+ ```tsx
184
+ import { useApolloClient } from "@apollo/client/react";
185
+
186
+ function CreatePost() {
187
+ const client = useApolloClient();
188
+
189
+ async function handleSubmit(formData: FormData) {
190
+ try {
191
+ const { data } = await client.mutate({
192
+ mutation: CREATE_POST,
193
+ variables: {
194
+ input: {
195
+ title: formData.get("title"),
196
+ content: formData.get("content"),
197
+ },
198
+ },
199
+ });
200
+ console.log("Created:", data.createPost);
201
+ router.push(`/posts/${data.createPost.id}`);
202
+ } catch (error) {
203
+ console.error("Failed to create post:", error);
204
+ }
205
+ }
206
+
207
+ return (
208
+ <form
209
+ onSubmit={(e) => {
210
+ e.preventDefault();
211
+ handleSubmit(new FormData(e.currentTarget));
212
+ }}
213
+ >
214
+ ...
215
+ </form>
216
+ );
217
+ }
218
+ ```
219
+
220
+ If you do use the hook's state, e.g. because you want to render the `loading` state, errors or returned `data`, you can also use the `useMutation` hook with `async..await` in your handler:
221
+
222
+ ```tsx
223
+ function CreatePost() {
224
+ const [createPost, { loading }] = useMutation(CREATE_POST);
225
+
226
+ async function handleSubmit(formData: FormData) {
227
+ try {
228
+ const { data } = await createPost({
229
+ variables: {
230
+ input: {
231
+ title: formData.get("title"),
232
+ content: formData.get("content"),
233
+ },
234
+ },
235
+ });
236
+ console.log("Created:", data.createPost);
237
+ router.push(`/posts/${data.createPost.id}`);
238
+ } catch (error) {
239
+ console.error("Failed to create post:", error);
240
+ }
241
+ }
242
+
243
+ return (
244
+ <form
245
+ onSubmit={(e) => {
246
+ e.preventDefault();
247
+ handleSubmit(new FormData(e.currentTarget));
248
+ }}
249
+ >
250
+ <button type="submit" disabled={loading}>
251
+ {loading ? "Creating..." : "Create Post"}
252
+ </button>
253
+ </form>
254
+ );
255
+ }
256
+ ```
257
+
258
+ ## Optimistic UI
259
+
260
+ Optimistic UI immediately reflects the expected result of a mutation before the server responds.
261
+
262
+ ### Basic Optimistic Response
263
+
264
+ **Important**: `optimisticResponse` needs to be a full valid response for the mutation. A partial result might result in subtle errors.
265
+
266
+ ```tsx
267
+ const [addTodo] = useMutation(ADD_TODO, {
268
+ optimisticResponse: {
269
+ addTodo: {
270
+ __typename: "Todo",
271
+ id: "temp-id",
272
+ text: "New todo",
273
+ completed: false,
274
+ },
275
+ },
276
+ });
277
+ ```
278
+
279
+ ### Dynamic Optimistic Response
280
+
281
+ ```tsx
282
+ function TodoList() {
283
+ const [addTodo] = useMutation(ADD_TODO);
284
+
285
+ const handleAdd = (text: string) => {
286
+ addTodo({
287
+ variables: { text },
288
+ optimisticResponse: {
289
+ addTodo: {
290
+ __typename: "Todo",
291
+ id: `temp-${Date.now()}`,
292
+ text,
293
+ completed: false,
294
+ },
295
+ },
296
+ });
297
+ };
298
+
299
+ return <AddTodoForm onAdd={handleAdd} />;
300
+ }
301
+ ```
302
+
303
+ ### Optimistic Response with Cache Update
304
+
305
+ ```tsx
306
+ const [toggleTodo] = useMutation(TOGGLE_TODO, {
307
+ optimisticResponse: ({ id }) => ({
308
+ toggleTodo: {
309
+ __typename: "Todo",
310
+ id,
311
+ completed: true, // Assume success
312
+ },
313
+ }),
314
+ update: (cache, { data }) => {
315
+ // This runs twice: once with optimistic data, once with server data
316
+ cache.modify({
317
+ id: cache.identify(data.toggleTodo),
318
+ fields: {
319
+ completed: () => data.toggleTodo.completed,
320
+ },
321
+ });
322
+ },
323
+ });
324
+ ```
325
+
326
+ ## Cache Updates
327
+
328
+ ### Using update Function
329
+
330
+ ```tsx
331
+ const [addTodo] = useMutation(ADD_TODO, {
332
+ update: (cache, { data }) => {
333
+ // Read existing todos from cache
334
+ const existingTodos = cache.readQuery<{ todos: Todo[] }>({
335
+ query: GET_TODOS,
336
+ });
337
+
338
+ // Write updated list back to cache
339
+ cache.writeQuery({
340
+ query: GET_TODOS,
341
+ data: {
342
+ todos: [...(existingTodos?.todos ?? []), data.addTodo],
343
+ },
344
+ });
345
+ },
346
+ });
347
+ ```
348
+
349
+ ### cache.modify
350
+
351
+ ```tsx
352
+ const [deleteTodo] = useMutation(DELETE_TODO, {
353
+ update: (cache, { data }) => {
354
+ cache.modify({
355
+ fields: {
356
+ todos: (existingTodos: Reference[], { readField }) => {
357
+ return existingTodos.filter(
358
+ (todoRef) => readField("id", todoRef) !== data.deleteTodo.id
359
+ );
360
+ },
361
+ },
362
+ });
363
+ },
364
+ });
365
+ ```
366
+
367
+ ### cache.evict
368
+
369
+ ```tsx
370
+ const [deleteUser] = useMutation(DELETE_USER, {
371
+ update: (cache, { data }) => {
372
+ // Remove the user object from cache entirely
373
+ cache.evict({ id: cache.identify(data.deleteUser) });
374
+ // Clean up dangling references
375
+ cache.gc();
376
+ },
377
+ });
378
+ ```
379
+
380
+ ### Updating Related Queries
381
+
382
+ ```tsx
383
+ const [createPost] = useMutation(CREATE_POST, {
384
+ update: (cache, { data }) => {
385
+ // Update author's post count
386
+ cache.modify({
387
+ id: cache.identify({ __typename: "User", id: data.createPost.authorId }),
388
+ fields: {
389
+ postCount: (existing) => existing + 1,
390
+ posts: (existing, { toReference }) => [
391
+ ...existing,
392
+ toReference(data.createPost),
393
+ ],
394
+ },
395
+ });
396
+
397
+ // Add to feed
398
+ cache.modify({
399
+ fields: {
400
+ feed: (existing, { toReference }) => [
401
+ toReference(data.createPost),
402
+ ...existing,
403
+ ],
404
+ },
405
+ });
406
+ },
407
+ });
408
+ ```
409
+
410
+ ## Refetch Queries
411
+
412
+ ### Basic Refetch
413
+
414
+ There are three refetch notations:
415
+
416
+ - **String**: `refetchQueries: ['getTodos']` - refetches all active `getTodos` queries
417
+ - **Query document**: `refetchQueries: [GET_TODOS]` - refetches all active queries using this document
418
+ - **Object**: `refetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }]` - **fetches** the query, regardless if it's actively used in the UI
419
+
420
+ ```tsx
421
+ const [addTodo] = useMutation(ADD_TODO, {
422
+ // Refetch all active GET_TODOS queries
423
+ refetchQueries: ["getTodos"],
424
+ // Or: refetchQueries: [GET_TODOS],
425
+ });
426
+
427
+ // Fetch specific query with variables (even if not active)
428
+ const [addTodo] = useMutation(ADD_TODO, {
429
+ refetchQueries: [{ query: GET_TODOS }, { query: GET_TODO_COUNT }],
430
+ });
431
+ ```
432
+
433
+ ### Conditional Refetch
434
+
435
+ ```tsx
436
+ const [addTodo] = useMutation(ADD_TODO, {
437
+ refetchQueries: (result) => {
438
+ if (result.data?.addTodo.priority === "HIGH") {
439
+ return [{ query: GET_HIGH_PRIORITY_TODOS }];
440
+ }
441
+ return [{ query: GET_TODOS }];
442
+ },
443
+ });
444
+ ```
445
+
446
+ ### Refetch Active Queries
447
+
448
+ ```tsx
449
+ const [addTodo] = useMutation(ADD_TODO, {
450
+ refetchQueries: "active", // Refetch all active queries
451
+ // Or: 'all' to refetch all queries (including inactive)
452
+ });
453
+ ```
454
+
455
+ ### awaitRefetchQueries
456
+
457
+ ```tsx
458
+ const [addTodo] = useMutation(ADD_TODO, {
459
+ refetchQueries: [{ query: GET_TODOS }],
460
+ awaitRefetchQueries: true, // Wait for refetch before resolving mutation
461
+ });
462
+ ```
463
+
464
+ ### onQueryUpdated
465
+
466
+ Returning `true` from `onQueryUpdated` causes a refetch. Don't call `refetch()` manually inside `onQueryUpdated`, as it won't retain the query and might cancel it early.
467
+
468
+ ```tsx
469
+ const [addTodo] = useMutation(ADD_TODO, {
470
+ update: (cache, { data }) => {
471
+ // Update cache...
472
+ },
473
+ onQueryUpdated: (observableQuery) => {
474
+ // Called for each query affected by cache update
475
+ console.log(`Query ${observableQuery.queryName} was updated`);
476
+ // Return true to refetch
477
+ return true;
478
+ },
479
+ });
480
+ ```
481
+
482
+ ## Error Handling
483
+
484
+ ### Error Policy
485
+
486
+ ```tsx
487
+ const [createUser, { loading }] = useMutation(CREATE_USER, {
488
+ errorPolicy: "all", // Return both data and errors
489
+ });
490
+
491
+ const { data, errors } = await createUser({
492
+ variables: { input },
493
+ });
494
+
495
+ // Handle partial success
496
+ if (data?.createUser) {
497
+ console.log("User created:", data.createUser);
498
+ }
499
+ if (errors) {
500
+ console.warn("Some errors occurred:", errors);
501
+ }
502
+ ```
503
+
504
+ ### onError Callback
505
+
506
+ ```tsx
507
+ const [createUser] = useMutation(CREATE_USER, {
508
+ onError: (error) => {
509
+ // Handle error globally
510
+ toast.error(`Failed to create user: ${error.message}`);
511
+
512
+ // Log to error tracking service
513
+ Sentry.captureException(error);
514
+ },
515
+ onCompleted: (data) => {
516
+ toast.success(`User ${data.createUser.name} created!`);
517
+ },
518
+ });
519
+ ```
520
+
521
+ ### Field-Level Errors
522
+
523
+ ```tsx
524
+ const [createUser] = useMutation(CREATE_USER, {
525
+ errorPolicy: "all",
526
+ });
527
+
528
+ const handleSubmit = async (input: CreateUserInput) => {
529
+ const { data, errors } = await createUser({
530
+ variables: { input },
531
+ });
532
+
533
+ // Handle GraphQL validation errors
534
+ const fieldErrors = errors?.reduce(
535
+ (acc, error) => {
536
+ const field = error.extensions?.field as string;
537
+ if (field) {
538
+ acc[field] = error.message;
539
+ }
540
+ return acc;
541
+ },
542
+ {} as Record<string, string>
543
+ );
544
+
545
+ if (fieldErrors?.email) {
546
+ setEmailError(fieldErrors.email);
547
+ }
548
+ };
549
+ ```