@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,773 @@
|
|
|
1
|
+
# Suspense Hooks Reference
|
|
2
|
+
|
|
3
|
+
> **Note**: Suspense hooks are the recommended approach for data fetching in modern React applications (React 18+). They provide cleaner code, better loading state handling, and enable streaming SSR.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [useSuspenseQuery Hook](#usesuspensequery-hook)
|
|
8
|
+
- [useBackgroundQuery and useReadQuery](#usebackgroundquery-and-usereadquery)
|
|
9
|
+
- [useLoadableQuery](#useloadablequery)
|
|
10
|
+
- [createQueryPreloader](#createquerypreloader)
|
|
11
|
+
- [useQueryRefHandlers](#usequeryrefhandlers)
|
|
12
|
+
- [Distinguishing Queries with queryKey](#distinguishing-queries-with-querykey)
|
|
13
|
+
- [Suspense Boundaries and Error Handling](#suspense-boundaries-and-error-handling)
|
|
14
|
+
- [Transitions](#transitions)
|
|
15
|
+
- [Avoiding Request Waterfalls](#avoiding-request-waterfalls)
|
|
16
|
+
- [Fetch Policies](#fetch-policies)
|
|
17
|
+
- [Streaming SSR or React Server Components](#streaming-ssr-or-react-server-components)
|
|
18
|
+
- [Conditional Queries](#conditional-queries)
|
|
19
|
+
|
|
20
|
+
## useSuspenseQuery Hook
|
|
21
|
+
|
|
22
|
+
The `useSuspenseQuery` hook is the Suspense-ready replacement for `useQuery`. It initiates a network request and causes the component calling it to suspend while the request is made. Unlike `useQuery`, it does not return `loading` states—these are handled by React's Suspense boundaries and error boundaries.
|
|
23
|
+
|
|
24
|
+
### Basic Usage
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Suspense } from "react";
|
|
28
|
+
import { useSuspenseQuery } from "@apollo/client/react";
|
|
29
|
+
import { GET_DOG } from "./queries.generated";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
return (
|
|
33
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
34
|
+
<Dog id="3" />
|
|
35
|
+
</Suspense>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Dog({ id }: { id: string }) {
|
|
40
|
+
const { data } = useSuspenseQuery(GET_DOG, {
|
|
41
|
+
variables: { id },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// data is always defined when this component renders
|
|
45
|
+
return <div>Name: {data.dog.name}</div>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Return Object
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const {
|
|
53
|
+
data, // Query result data
|
|
54
|
+
dataState, // With default options: "complete" | "streaming"
|
|
55
|
+
// With returnPartialData: also "partial"
|
|
56
|
+
// With errorPolicy "all" or "ignore": also "empty"
|
|
57
|
+
error, // ApolloError (only when errorPolicy is "all" or "ignore")
|
|
58
|
+
networkStatus, // NetworkStatus.ready, NetworkStatus.loading, etc.
|
|
59
|
+
client, // Apollo Client instance
|
|
60
|
+
refetch, // Function to re-execute query
|
|
61
|
+
fetchMore, // Function for pagination
|
|
62
|
+
} = useSuspenseQuery(QUERY, options);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Key Differences from useQuery
|
|
66
|
+
|
|
67
|
+
- **No `loading` boolean**: Component suspends instead of returning `loading: true`
|
|
68
|
+
- **Error handling**: With default `errorPolicy` (`none`), errors are thrown and caught by error boundaries. With `errorPolicy: "all"` or `"ignore"`, the `error` property is returned and `data` may be `undefined`.
|
|
69
|
+
- **`data` availability**: With default `errorPolicy` (`none`), `data` is guaranteed to be present when the component renders. With `errorPolicy: "all"` or `"ignore"`, when `dataState` is `empty`, `data` may be `undefined`.
|
|
70
|
+
- **Suspense boundaries**: Must wrap component with `<Suspense>` to handle loading state
|
|
71
|
+
|
|
72
|
+
### Changing Variables
|
|
73
|
+
|
|
74
|
+
When variables change, `useSuspenseQuery` automatically re-runs the query. If the data is not in the cache, the component suspends again.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { useState } from "react";
|
|
78
|
+
import { GET_DOGS } from "./queries.generated";
|
|
79
|
+
|
|
80
|
+
function DogSelector() {
|
|
81
|
+
const { data } = useSuspenseQuery(GET_DOGS);
|
|
82
|
+
const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<select
|
|
87
|
+
value={selectedDog}
|
|
88
|
+
onChange={(e) => setSelectedDog(e.target.value)}
|
|
89
|
+
>
|
|
90
|
+
{data.dogs.map((dog) => (
|
|
91
|
+
<option key={dog.id} value={dog.id}>
|
|
92
|
+
{dog.name}
|
|
93
|
+
</option>
|
|
94
|
+
))}
|
|
95
|
+
</select>
|
|
96
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
97
|
+
<Dog id={selectedDog} />
|
|
98
|
+
</Suspense>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Dog({ id }: { id: string }) {
|
|
104
|
+
const { data } = useSuspenseQuery(GET_DOG, {
|
|
105
|
+
variables: { id },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<div>Name: {data.dog.name}</div>
|
|
111
|
+
<div>Breed: {data.dog.breed}</div>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Rendering Partial Data
|
|
118
|
+
|
|
119
|
+
Use `returnPartialData` to render immediately with partial cache data instead of suspending. The component will still suspend if there is no data in the cache.
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
function Dog({ id }: { id: string }) {
|
|
123
|
+
const { data } = useSuspenseQuery(GET_DOG, {
|
|
124
|
+
variables: { id },
|
|
125
|
+
returnPartialData: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
<div>Name: {data.dog?.name ?? "Unknown"}</div>
|
|
131
|
+
{data.dog?.breed && <div>Breed: {data.dog.breed}</div>}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## useBackgroundQuery and useReadQuery
|
|
138
|
+
|
|
139
|
+
Use `useBackgroundQuery` with `useReadQuery` to avoid request waterfalls by starting a query in a parent component and reading the result in a child component. This pattern enables the parent to start fetching data before the child component renders.
|
|
140
|
+
|
|
141
|
+
### Basic Usage
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { Suspense } from "react";
|
|
145
|
+
import { useBackgroundQuery, useReadQuery } from "@apollo/client/react";
|
|
146
|
+
|
|
147
|
+
function Parent() {
|
|
148
|
+
// Start fetching immediately
|
|
149
|
+
const [queryRef] = useBackgroundQuery(GET_DOG, {
|
|
150
|
+
variables: { id: "3" },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
155
|
+
<Child queryRef={queryRef} />
|
|
156
|
+
</Suspense>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function Child({ queryRef }: { queryRef: QueryRef<DogData> }) {
|
|
161
|
+
// Read the query result
|
|
162
|
+
const { data } = useReadQuery(queryRef);
|
|
163
|
+
|
|
164
|
+
return <div>Name: {data.dog.name}</div>;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### When to Use
|
|
169
|
+
|
|
170
|
+
- **Avoiding waterfalls**: Start fetching data in a parent (preferably above a suspense boundary) before child components render
|
|
171
|
+
- **Preloading data**: Begin fetching before the component that needs the data is ready
|
|
172
|
+
- **Parallel queries**: Start multiple queries at once in a parent component
|
|
173
|
+
|
|
174
|
+
### Return Values
|
|
175
|
+
|
|
176
|
+
`useBackgroundQuery` returns a tuple:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const [
|
|
180
|
+
queryRef, // QueryRef to pass to useReadQuery
|
|
181
|
+
{ refetch, fetchMore, subscribeToMore }, // Helper functions
|
|
182
|
+
] = useBackgroundQuery(QUERY, options);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`useReadQuery` returns the query result:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const {
|
|
189
|
+
data, // Query result data (always defined)
|
|
190
|
+
dataState, // "complete" | "streaming" | "partial" | "empty"
|
|
191
|
+
error, // ApolloError (if errorPolicy allows)
|
|
192
|
+
networkStatus, // Detailed network state (1-8)
|
|
193
|
+
} = useReadQuery(queryRef);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## useLoadableQuery
|
|
197
|
+
|
|
198
|
+
Use `useLoadableQuery` to imperatively load a query in response to a user interaction (like a button click) instead of on component mount.
|
|
199
|
+
|
|
200
|
+
### Basic Usage
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Suspense } from "react";
|
|
204
|
+
import { useLoadableQuery, useReadQuery } from "@apollo/client/react";
|
|
205
|
+
import { GET_GREETING } from "./queries.generated";
|
|
206
|
+
|
|
207
|
+
function App() {
|
|
208
|
+
const [loadGreeting, queryRef] = useLoadableQuery(GET_GREETING);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => loadGreeting({ variables: { language: "english" } })}
|
|
214
|
+
>
|
|
215
|
+
Load Greeting
|
|
216
|
+
</button>
|
|
217
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
218
|
+
{queryRef && <Greeting queryRef={queryRef} />}
|
|
219
|
+
</Suspense>
|
|
220
|
+
</>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function Greeting({ queryRef }: { queryRef: QueryRef<GreetingData> }) {
|
|
225
|
+
const { data } = useReadQuery(queryRef);
|
|
226
|
+
|
|
227
|
+
return <div>{data.greeting.message}</div>;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Return Values
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const [
|
|
235
|
+
loadQuery, // Function to load the query
|
|
236
|
+
queryRef, // QueryRef (null until loadQuery is called)
|
|
237
|
+
{ refetch, fetchMore, subscribeToMore, reset }, // Helper functions
|
|
238
|
+
] = useLoadableQuery(QUERY, options);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### When to Use
|
|
242
|
+
|
|
243
|
+
- **User-triggered fetching**: Load data in response to user actions
|
|
244
|
+
- **Lazy loading**: Defer data fetching until it's actually needed
|
|
245
|
+
- **Progressive disclosure**: Load data for UI elements that may not be initially visible
|
|
246
|
+
|
|
247
|
+
## createQueryPreloader
|
|
248
|
+
|
|
249
|
+
The `createQueryPreloader` function creates a `preloadQuery` function that can be used to initiate queries outside of React components. This is useful for preloading data before a component renders, such as in route loaders or event handlers.
|
|
250
|
+
|
|
251
|
+
### Basic Usage
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
|
255
|
+
import { createQueryPreloader } from "@apollo/client/react";
|
|
256
|
+
|
|
257
|
+
const client = new ApolloClient({
|
|
258
|
+
uri: "https://your-graphql-endpoint.com/graphql",
|
|
259
|
+
cache: new InMemoryCache(),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Create a preload function
|
|
263
|
+
export const preloadQuery = createQueryPreloader(client);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Using preloadQuery with Route Loaders
|
|
267
|
+
|
|
268
|
+
> **Note**: This example applies to React Router in non-framework mode. For React Router framework mode, see [integration-react-router.md](./integration-react-router.md).
|
|
269
|
+
|
|
270
|
+
Use the preload function with React Router's `loader` function to begin loading data during route transitions:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { preloadQuery } from "@/lib/apollo-client";
|
|
274
|
+
import { GET_DOG } from "./queries.generated";
|
|
275
|
+
|
|
276
|
+
// React Router loader function
|
|
277
|
+
export async function loader({ params }: { params: { id: string } }) {
|
|
278
|
+
return preloadQuery({
|
|
279
|
+
query: GET_DOG,
|
|
280
|
+
variables: { id: params.id },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Route component
|
|
285
|
+
export default function DogRoute() {
|
|
286
|
+
const queryRef = useLoaderData();
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
290
|
+
<DogDetails queryRef={queryRef} />
|
|
291
|
+
</Suspense>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function DogDetails({ queryRef }: { queryRef: QueryRef<DogData> }) {
|
|
296
|
+
const { data } = useReadQuery(queryRef);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div>
|
|
300
|
+
<h1>{data.dog.name}</h1>
|
|
301
|
+
<p>Breed: {data.dog.breed}</p>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Preventing Route Transitions Until Query Loads
|
|
308
|
+
|
|
309
|
+
Use `preloadQuery.toPromise(queryRef)` to prevent route transitions until the query finishes loading:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
export async function loader({ params }: { params: { id: string } }) {
|
|
313
|
+
const queryRef = preloadQuery({
|
|
314
|
+
query: GET_DOG,
|
|
315
|
+
variables: { id: params.id },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Wait for the query to complete before transitioning
|
|
319
|
+
return preloadQuery.toPromise(queryRef);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
When `preloadQuery.toPromise()` is used, the route transition waits for the query to complete, and the data renders immediately without showing a loading fallback.
|
|
324
|
+
|
|
325
|
+
> **Note**: `preloadQuery.toPromise()` resolves with the `queryRef` itself (not the data) to encourage using `useReadQuery` for cache updates. If you need raw query data in your loader, use `client.query()` directly.
|
|
326
|
+
|
|
327
|
+
### With Next.js Server Components
|
|
328
|
+
|
|
329
|
+
> **Note**: For Next.js App Router, use the `PreloadQuery` component from `@apollo/client-integration-nextjs` instead. See [integration-nextjs.md](./integration-nextjs.md) for details.
|
|
330
|
+
|
|
331
|
+
## useQueryRefHandlers
|
|
332
|
+
|
|
333
|
+
The `useQueryRefHandlers` hook provides access to `refetch` and `fetchMore` functions for queries initiated with `preloadQuery`, `useBackgroundQuery`, or `useLoadableQuery`. This is useful when you need to refetch or paginate data in components where the `queryRef` is passed through.
|
|
334
|
+
|
|
335
|
+
> **Important:** Always call `useQueryRefHandlers` before `useReadQuery`. These two hooks interact with the same `queryRef`, and calling them in the wrong order could cause subtle bugs.
|
|
336
|
+
|
|
337
|
+
### Basic Usage
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { useQueryRefHandlers } from "@apollo/client/react";
|
|
341
|
+
|
|
342
|
+
function Breeds({ queryRef }: { queryRef: QueryRef<BreedsData> }) {
|
|
343
|
+
const { refetch } = useQueryRefHandlers(queryRef);
|
|
344
|
+
const { data } = useReadQuery(queryRef);
|
|
345
|
+
const [isPending, startTransition] = useTransition();
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div>
|
|
349
|
+
<button
|
|
350
|
+
disabled={isPending}
|
|
351
|
+
onClick={() => {
|
|
352
|
+
startTransition(() => {
|
|
353
|
+
refetch();
|
|
354
|
+
});
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{isPending ? "Refetching..." : "Refetch breeds"}
|
|
358
|
+
</button>
|
|
359
|
+
<ul>
|
|
360
|
+
{data.breeds.map((breed) => (
|
|
361
|
+
<li key={breed.id}>{breed.name}</li>
|
|
362
|
+
))}
|
|
363
|
+
</ul>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### With Pagination
|
|
370
|
+
|
|
371
|
+
Use `fetchMore` to implement pagination:
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
function Posts({ queryRef }: { queryRef: QueryRef<PostsData> }) {
|
|
375
|
+
const { fetchMore } = useQueryRefHandlers(queryRef);
|
|
376
|
+
const { data } = useReadQuery(queryRef);
|
|
377
|
+
const [isPending, startTransition] = useTransition();
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div>
|
|
381
|
+
<ul>
|
|
382
|
+
{data.posts.map((post) => (
|
|
383
|
+
<li key={post.id}>{post.title}</li>
|
|
384
|
+
))}
|
|
385
|
+
</ul>
|
|
386
|
+
<button
|
|
387
|
+
disabled={isPending}
|
|
388
|
+
onClick={() => {
|
|
389
|
+
startTransition(() => {
|
|
390
|
+
fetchMore({
|
|
391
|
+
variables: {
|
|
392
|
+
offset: data.posts.length,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
{isPending ? "Loading..." : "Load more"}
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### When to Use
|
|
406
|
+
|
|
407
|
+
- **Preloaded queries**: Access refetch/fetchMore for queries initiated with `preloadQuery`
|
|
408
|
+
- **Background queries**: Use in child components receiving `queryRef` from `useBackgroundQuery`
|
|
409
|
+
- **Loadable queries**: Refetch or paginate queries initiated with `useLoadableQuery`
|
|
410
|
+
- **React transitions**: Integrate with transitions to avoid showing loading fallbacks during refetches
|
|
411
|
+
|
|
412
|
+
## Distinguishing Queries with queryKey
|
|
413
|
+
|
|
414
|
+
Apollo Client uses the combination of `query` and `variables` to uniquely identify each query. When multiple components use the same `query` and `variables`, they share the same identity and suspend at the same time, regardless of which component initiates the request.
|
|
415
|
+
|
|
416
|
+
Use the `queryKey` option to ensure each hook has a unique identity:
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
function UserProfile() {
|
|
420
|
+
// First query with unique key
|
|
421
|
+
const { data: userData } = useSuspenseQuery(GET_USER, {
|
|
422
|
+
variables: { id: "1" },
|
|
423
|
+
queryKey: ["user-profile"],
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Second query with same query and variables but different key
|
|
427
|
+
const { data: userPreview } = useSuspenseQuery(GET_USER, {
|
|
428
|
+
variables: { id: "1" },
|
|
429
|
+
queryKey: ["user-preview"],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<div>
|
|
434
|
+
<UserCard user={userData.user} />
|
|
435
|
+
<UserSidebar user={userPreview.user} />
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### When to Use
|
|
442
|
+
|
|
443
|
+
- **Multiple instances**: When rendering multiple components that use the same query and variables
|
|
444
|
+
- **Preventing shared suspension**: When you want independent control over when each query suspends
|
|
445
|
+
- **Separate cache entries**: When you need to maintain separate cache states for the same query
|
|
446
|
+
|
|
447
|
+
> **Note**: Each item in the `queryKey` array must be a stable identifier to prevent infinite fetches.
|
|
448
|
+
|
|
449
|
+
## Suspense Boundaries and Error Handling
|
|
450
|
+
|
|
451
|
+
### Suspense Boundaries
|
|
452
|
+
|
|
453
|
+
Wrap components that use Suspense hooks with `<Suspense>` boundaries to handle loading states. Place boundaries strategically to control the granularity of loading indicators.
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
function App() {
|
|
457
|
+
return (
|
|
458
|
+
<>
|
|
459
|
+
{/* Top-level loading for entire page */}
|
|
460
|
+
<Suspense fallback={<PageSpinner />}>
|
|
461
|
+
<Header />
|
|
462
|
+
<Content />
|
|
463
|
+
</Suspense>
|
|
464
|
+
</>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function Content() {
|
|
469
|
+
return (
|
|
470
|
+
<>
|
|
471
|
+
<MainSection />
|
|
472
|
+
{/* Granular loading for sidebar */}
|
|
473
|
+
<Suspense fallback={<SidebarSkeleton />}>
|
|
474
|
+
<Sidebar />
|
|
475
|
+
</Suspense>
|
|
476
|
+
</>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Error Boundaries
|
|
482
|
+
|
|
483
|
+
Suspense hooks throw errors to React error boundaries instead of returning them. Use error boundaries to handle GraphQL errors.
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
487
|
+
|
|
488
|
+
function App() {
|
|
489
|
+
return (
|
|
490
|
+
<ErrorBoundary
|
|
491
|
+
fallback={({ error }) => (
|
|
492
|
+
<div>
|
|
493
|
+
<h2>Something went wrong</h2>
|
|
494
|
+
<p>{error.message}</p>
|
|
495
|
+
</div>
|
|
496
|
+
)}
|
|
497
|
+
>
|
|
498
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
499
|
+
<Dog id="3" />
|
|
500
|
+
</Suspense>
|
|
501
|
+
</ErrorBoundary>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Custom Error Policies
|
|
507
|
+
|
|
508
|
+
Use `errorPolicy` to control how errors are handled:
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
function Dog({ id }: { id: string }) {
|
|
512
|
+
const { data, error } = useSuspenseQuery(GET_DOG, {
|
|
513
|
+
variables: { id },
|
|
514
|
+
errorPolicy: "all", // Return both data and errors
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<>
|
|
519
|
+
<div>Name: {data?.dog?.name ?? "Unknown"}</div>
|
|
520
|
+
{error && <div>Warning: {error.message}</div>}
|
|
521
|
+
</>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Transitions
|
|
527
|
+
|
|
528
|
+
Use React transitions to avoid showing loading UI when updating state. Transitions keep the previous UI visible while new data is fetching.
|
|
529
|
+
|
|
530
|
+
### Using startTransition
|
|
531
|
+
|
|
532
|
+
```tsx
|
|
533
|
+
import { useState, Suspense, startTransition } from "react";
|
|
534
|
+
|
|
535
|
+
function DogSelector() {
|
|
536
|
+
const { data } = useSuspenseQuery(GET_DOGS);
|
|
537
|
+
const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<>
|
|
541
|
+
<select
|
|
542
|
+
value={selectedDog}
|
|
543
|
+
onChange={(e) => {
|
|
544
|
+
// Wrap state update in startTransition
|
|
545
|
+
startTransition(() => {
|
|
546
|
+
setSelectedDog(e.target.value);
|
|
547
|
+
});
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
{data.dogs.map((dog) => (
|
|
551
|
+
<option key={dog.id} value={dog.id}>
|
|
552
|
+
{dog.name}
|
|
553
|
+
</option>
|
|
554
|
+
))}
|
|
555
|
+
</select>
|
|
556
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
557
|
+
<Dog id={selectedDog} />
|
|
558
|
+
</Suspense>
|
|
559
|
+
</>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Using useTransition
|
|
565
|
+
|
|
566
|
+
Use `useTransition` to get an `isPending` flag for visual feedback during transitions.
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import { useState, Suspense, useTransition } from "react";
|
|
570
|
+
|
|
571
|
+
function DogSelector() {
|
|
572
|
+
const [isPending, startTransition] = useTransition();
|
|
573
|
+
const { data } = useSuspenseQuery(GET_DOGS);
|
|
574
|
+
const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
<>
|
|
578
|
+
<select
|
|
579
|
+
style={{ opacity: isPending ? 0.5 : 1 }}
|
|
580
|
+
value={selectedDog}
|
|
581
|
+
onChange={(e) => {
|
|
582
|
+
startTransition(() => {
|
|
583
|
+
setSelectedDog(e.target.value);
|
|
584
|
+
});
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
{data.dogs.map((dog) => (
|
|
588
|
+
<option key={dog.id} value={dog.id}>
|
|
589
|
+
{dog.name}
|
|
590
|
+
</option>
|
|
591
|
+
))}
|
|
592
|
+
</select>
|
|
593
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
594
|
+
<Dog id={selectedDog} />
|
|
595
|
+
</Suspense>
|
|
596
|
+
</>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Avoiding Request Waterfalls
|
|
602
|
+
|
|
603
|
+
Request waterfalls occur when a child component waits for the parent to finish rendering before it can start fetching its own data. Use `useBackgroundQuery` to start fetching child data earlier in the component tree.
|
|
604
|
+
|
|
605
|
+
> **Note**: When one query depends on the result of another query (e.g., the child query needs an ID from the parent query), the waterfall is unavoidable. The best solution is to restructure your schema to fetch all needed data in a single nested query.
|
|
606
|
+
|
|
607
|
+
### Example: Independent Queries
|
|
608
|
+
|
|
609
|
+
When queries don't depend on each other, use `useBackgroundQuery` to start them in parallel:
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
const GET_USER = gql`
|
|
613
|
+
query GetUser($id: String!) {
|
|
614
|
+
user(id: $id) {
|
|
615
|
+
id
|
|
616
|
+
name
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
`;
|
|
620
|
+
|
|
621
|
+
const GET_POSTS = gql`
|
|
622
|
+
query GetPosts {
|
|
623
|
+
posts {
|
|
624
|
+
id
|
|
625
|
+
title
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
`;
|
|
629
|
+
|
|
630
|
+
function Parent() {
|
|
631
|
+
// Both queries start immediately - no waterfall
|
|
632
|
+
const [userRef] = useBackgroundQuery(GET_USER, {
|
|
633
|
+
variables: { id: "1" },
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const [postsRef] = useBackgroundQuery(GET_POSTS);
|
|
637
|
+
|
|
638
|
+
return (
|
|
639
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
640
|
+
<UserProfile queryRef={userRef} />
|
|
641
|
+
<PostsList queryRef={postsRef} />
|
|
642
|
+
</Suspense>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function UserProfile({ queryRef }: { queryRef: QueryRef<UserData> }) {
|
|
647
|
+
const { data } = useReadQuery(queryRef);
|
|
648
|
+
|
|
649
|
+
return <div>User: {data.user.name}</div>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function PostsList({ queryRef }: { queryRef: QueryRef<PostsData> }) {
|
|
653
|
+
const { data } = useReadQuery(queryRef);
|
|
654
|
+
|
|
655
|
+
return (
|
|
656
|
+
<ul>
|
|
657
|
+
{data.posts.map((post) => (
|
|
658
|
+
<li key={post.id}>{post.title}</li>
|
|
659
|
+
))}
|
|
660
|
+
</ul>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## Fetch Policies
|
|
666
|
+
|
|
667
|
+
Suspense hooks support most of the same fetch policies as `useQuery`, controlling how the query interacts with the cache. Note that `cache-only` and `standby` are not supported by Suspense hooks.
|
|
668
|
+
|
|
669
|
+
| Policy | Description |
|
|
670
|
+
| ------------------- | ---------------------------------------------------------- |
|
|
671
|
+
| `cache-first` | Return cached data if available, otherwise fetch (default) |
|
|
672
|
+
| `cache-and-network` | Return cached data immediately, then fetch and update |
|
|
673
|
+
| `network-only` | Always fetch, update cache, ignore cached data |
|
|
674
|
+
| `no-cache` | Always fetch, never read or write cache |
|
|
675
|
+
|
|
676
|
+
### Usage Examples
|
|
677
|
+
|
|
678
|
+
```tsx
|
|
679
|
+
// Always fetch fresh data
|
|
680
|
+
const { data } = useSuspenseQuery(GET_NOTIFICATIONS, {
|
|
681
|
+
fetchPolicy: "network-only",
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Prefer cached data
|
|
685
|
+
const { data } = useSuspenseQuery(GET_CATEGORIES, {
|
|
686
|
+
fetchPolicy: "cache-first",
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Show cached data while fetching fresh data
|
|
690
|
+
const { data } = useSuspenseQuery(GET_POSTS, {
|
|
691
|
+
fetchPolicy: "cache-and-network",
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## Streaming SSR or React Server Components
|
|
696
|
+
|
|
697
|
+
Apollo Client integrates with modern React frameworks that support Streaming SSR and React Server Components. For detailed setup instructions specific to your framework, see:
|
|
698
|
+
|
|
699
|
+
- **Next.js App Router**: [integration-nextjs.md](./integration-nextjs.md) - Includes React Server Components, PreloadQuery component, and streaming SSR
|
|
700
|
+
- **React Router**: [integration-react-router.md](./integration-react-router.md) - Framework mode with SSR support
|
|
701
|
+
- **TanStack Start**: [integration-tanstack-start.md](./integration-tanstack-start.md) - Full-stack React framework with SSR
|
|
702
|
+
|
|
703
|
+
These guides cover:
|
|
704
|
+
|
|
705
|
+
- Framework-specific client setup and configuration
|
|
706
|
+
- Preloading queries for optimal performance
|
|
707
|
+
- Streaming SSR with `useBackgroundQuery` and Suspense
|
|
708
|
+
- Error handling in server-rendered environments
|
|
709
|
+
|
|
710
|
+
## Conditional Queries
|
|
711
|
+
|
|
712
|
+
### Using skipToken
|
|
713
|
+
|
|
714
|
+
Use `skipToken` to conditionally skip queries without TypeScript issues. When `skipToken` is used, the component won't suspend and `data` will be `undefined`.
|
|
715
|
+
|
|
716
|
+
```tsx
|
|
717
|
+
import { skipToken } from "@apollo/client";
|
|
718
|
+
|
|
719
|
+
const GET_USER = gql`
|
|
720
|
+
query GetUser($id: ID!) {
|
|
721
|
+
user(id: $id) {
|
|
722
|
+
id
|
|
723
|
+
name
|
|
724
|
+
email
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
function UserProfile({ userId }: { userId: string | null }) {
|
|
730
|
+
const { data, dataState } = useSuspenseQuery(
|
|
731
|
+
GET_USER,
|
|
732
|
+
!userId ? skipToken : (
|
|
733
|
+
{
|
|
734
|
+
variables: { id: userId },
|
|
735
|
+
}
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
if (dataState !== "complete") {
|
|
740
|
+
return <p>Select a user</p>;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return <Profile user={data.user} />;
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Conditional Rendering
|
|
748
|
+
|
|
749
|
+
Alternatively, use conditional rendering to control when Suspense hooks are called. This provides better type safety and clearer component logic.
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
function UserProfile({ userId }: { userId: string | null }) {
|
|
753
|
+
if (!userId) {
|
|
754
|
+
return <p>Select a user</p>;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return (
|
|
758
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
759
|
+
<UserDetails userId={userId} />
|
|
760
|
+
</Suspense>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function UserDetails({ userId }: { userId: string }) {
|
|
765
|
+
const { data } = useSuspenseQuery(GET_USER, {
|
|
766
|
+
variables: { id: userId },
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
return <Profile user={data.user} />;
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
> **Note**: Using conditional rendering with `skipToken` provides better type safety and avoids issues with required variables. The `skip` option is deprecated in favor of `skipToken`.
|