0
# Provider & Context System
1
2
Components and hooks for managing QueryClient context, hydration, and error boundaries.
3
4
## QueryClientProvider
5
6
**Provider component for QueryClient context**
7
8
```typescript { .api }
9
const QueryClientProvider: React.FC<QueryClientProviderProps>
10
11
interface QueryClientProviderProps {
12
client: QueryClient
13
children?: React.ReactNode
14
}
15
```
16
17
### Basic Setup
18
19
```typescript { .api }
20
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
21
22
// Create a client
23
const queryClient = new QueryClient({
24
defaultOptions: {
25
queries: {
26
staleTime: 5 * 60 * 1000, // 5 minutes
27
gcTime: 10 * 60 * 1000, // 10 minutes
28
retry: (failureCount, error) => {
29
if (error.status === 404) return false
30
return failureCount < 3
31
}
32
},
33
mutations: {
34
retry: 1
35
}
36
}
37
})
38
39
function App() {
40
return (
41
<QueryClientProvider client={queryClient}>
42
<div className="app">
43
<Router>
44
<Routes>
45
<Route path="/" element={<Home />} />
46
<Route path="/users/:id" element={<UserProfile />} />
47
</Routes>
48
</Router>
49
</div>
50
</QueryClientProvider>
51
)
52
}
53
```
54
55
### Advanced Configuration
56
57
```typescript { .api }
58
import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from '@tanstack/react-query'
59
60
const queryClient = new QueryClient({
61
queryCache: new QueryCache({
62
onError: (error, query) => {
63
console.error(`Query failed:`, error)
64
// Global query error handling
65
if (error.status === 401) {
66
// Redirect to login
67
window.location.href = '/login'
68
}
69
},
70
onSuccess: (data, query) => {
71
// Global success handling
72
console.log(`Query succeeded for key:`, query.queryKey)
73
}
74
}),
75
mutationCache: new MutationCache({
76
onError: (error, variables, context, mutation) => {
77
console.error(`Mutation failed:`, error)
78
// Show global error notification
79
toast.error(`Operation failed: ${error.message}`)
80
},
81
onSuccess: (data, variables, context, mutation) => {
82
// Show global success notification
83
if (mutation.options.meta?.successMessage) {
84
toast.success(mutation.options.meta.successMessage)
85
}
86
}
87
}),
88
defaultOptions: {
89
queries: {
90
staleTime: 5 * 60 * 1000,
91
gcTime: 10 * 60 * 1000,
92
refetchOnWindowFocus: false,
93
retry: (failureCount, error) => {
94
// Custom retry logic
95
if (error.status === 404 || error.status === 403) return false
96
return failureCount < 3
97
}
98
},
99
mutations: {
100
retry: (failureCount, error) => {
101
if (error.status >= 400 && error.status < 500) return false
102
return failureCount < 2
103
}
104
}
105
}
106
})
107
108
function App() {
109
return (
110
<QueryClientProvider client={queryClient}>
111
<ErrorBoundary>
112
<Suspense fallback={<GlobalLoadingSpinner />}>
113
<AppContent />
114
</Suspense>
115
</ErrorBoundary>
116
</QueryClientProvider>
117
)
118
}
119
```
120
121
## QueryClientContext
122
123
**React context that holds the QueryClient instance**
124
125
```typescript { .api }
126
const QueryClientContext: React.Context<QueryClient | undefined>
127
```
128
129
### Direct Context Usage
130
131
```typescript { .api }
132
import { useContext } from 'react'
133
import { QueryClientContext } from '@tanstack/react-query'
134
135
function MyComponent() {
136
const queryClient = useContext(QueryClientContext)
137
138
if (!queryClient) {
139
throw new Error('QueryClient not found. Make sure you are using QueryClientProvider.')
140
}
141
142
const handleInvalidate = () => {
143
queryClient.invalidateQueries({ queryKey: ['posts'] })
144
}
145
146
return (
147
<button onClick={handleInvalidate}>
148
Refresh Posts
149
</button>
150
)
151
}
152
```
153
154
## useQueryClient
155
156
**Hook to access the current QueryClient instance**
157
158
```typescript { .api }
159
function useQueryClient(queryClient?: QueryClient): QueryClient
160
```
161
162
### Basic Usage
163
164
```typescript { .api }
165
import { useQueryClient } from '@tanstack/react-query'
166
167
function RefreshButton() {
168
const queryClient = useQueryClient()
169
170
const handleRefreshAll = () => {
171
// Invalidate all queries
172
queryClient.invalidateQueries()
173
}
174
175
const handleRefreshPosts = () => {
176
// Invalidate specific queries
177
queryClient.invalidateQueries({ queryKey: ['posts'] })
178
}
179
180
return (
181
<div>
182
<button onClick={handleRefreshPosts}>Refresh Posts</button>
183
<button onClick={handleRefreshAll}>Refresh All</button>
184
</div>
185
)
186
}
187
```
188
189
### Cache Manipulation
190
191
```typescript { .api }
192
function PostActions({ postId }: { postId: number }) {
193
const queryClient = useQueryClient()
194
195
const handleOptimisticUpdate = () => {
196
queryClient.setQueryData(['post', postId], (oldPost: Post) => ({
197
...oldPost,
198
likes: oldPost.likes + 1
199
}))
200
}
201
202
const handlePrefetch = () => {
203
queryClient.prefetchQuery({
204
queryKey: ['post-comments', postId],
205
queryFn: () => fetchPostComments(postId),
206
staleTime: 5 * 60 * 1000
207
})
208
}
209
210
const handleRemoveFromCache = () => {
211
queryClient.removeQueries({ queryKey: ['post', postId] })
212
}
213
214
return (
215
<div>
216
<button onClick={handleOptimisticUpdate}>Like</button>
217
<button onClick={handlePrefetch}>Prefetch Comments</button>
218
<button onClick={handleRemoveFromCache}>Remove from Cache</button>
219
</div>
220
)
221
}
222
```
223
224
### Manual Cache Updates
225
226
```typescript { .api }
227
function usePostCache() {
228
const queryClient = useQueryClient()
229
230
const updatePost = (postId: number, updates: Partial<Post>) => {
231
queryClient.setQueryData(['post', postId], (oldPost: Post) => ({
232
...oldPost,
233
...updates
234
}))
235
}
236
237
const addPost = (newPost: Post) => {
238
// Update posts list
239
queryClient.setQueryData(['posts'], (oldPosts: Post[]) =>
240
[newPost, ...oldPosts]
241
)
242
243
// Set individual post data
244
queryClient.setQueryData(['post', newPost.id], newPost)
245
}
246
247
const removePost = (postId: number) => {
248
// Remove from posts list
249
queryClient.setQueryData(['posts'], (oldPosts: Post[]) =>
250
oldPosts.filter(post => post.id !== postId)
251
)
252
253
// Remove individual post data
254
queryClient.removeQueries({ queryKey: ['post', postId] })
255
}
256
257
return { updatePost, addPost, removePost }
258
}
259
```
260
261
## HydrationBoundary
262
263
**Component for hydrating server-side rendered queries on the client**
264
265
```typescript { .api }
266
const HydrationBoundary: React.FC<HydrationBoundaryProps>
267
268
interface HydrationBoundaryProps {
269
state: DehydratedState | null | undefined
270
options?: HydrateOptions
271
children?: React.ReactNode
272
queryClient?: QueryClient
273
}
274
```
275
276
### Basic SSR Setup
277
278
```typescript { .api }
279
// Server-side (Next.js example)
280
import { QueryClient, dehydrate } from '@tanstack/react-query'
281
282
export async function getServerSideProps() {
283
const queryClient = new QueryClient()
284
285
// Prefetch data on the server
286
await queryClient.prefetchQuery({
287
queryKey: ['posts'],
288
queryFn: fetchPosts
289
})
290
291
await queryClient.prefetchQuery({
292
queryKey: ['user', 'me'],
293
queryFn: fetchCurrentUser
294
})
295
296
return {
297
props: {
298
dehydratedState: dehydrate(queryClient)
299
}
300
}
301
}
302
303
// Client-side
304
function PostsPage({ dehydratedState }: { dehydratedState: DehydratedState }) {
305
return (
306
<HydrationBoundary state={dehydratedState}>
307
<PostsList />
308
<UserProfile />
309
</HydrationBoundary>
310
)
311
}
312
```
313
314
### Advanced Hydration
315
316
```typescript { .api }
317
function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
318
const [queryClient] = useState(() => new QueryClient({
319
defaultOptions: {
320
queries: {
321
staleTime: 60 * 1000 // 1 minute
322
}
323
}
324
}))
325
326
return (
327
<QueryClientProvider client={queryClient}>
328
<HydrationBoundary
329
state={dehydratedState}
330
options={{
331
defaultOptions: {
332
queries: {
333
staleTime: 5 * 60 * 1000 // Override stale time for hydrated queries
334
}
335
}
336
}}
337
>
338
<Router>
339
<Routes>
340
<Route path="/" element={<HomePage />} />
341
<Route path="/posts" element={<PostsPage />} />
342
</Routes>
343
</Router>
344
</HydrationBoundary>
345
</QueryClientProvider>
346
)
347
}
348
```
349
350
### Selective Hydration
351
352
```typescript { .api }
353
// Server-side with selective dehydration
354
export async function getServerSideProps() {
355
const queryClient = new QueryClient()
356
357
await queryClient.prefetchQuery({
358
queryKey: ['posts'],
359
queryFn: fetchPosts
360
})
361
362
await queryClient.prefetchQuery({
363
queryKey: ['user-settings'],
364
queryFn: fetchUserSettings
365
})
366
367
return {
368
props: {
369
dehydratedState: dehydrate(queryClient, {
370
shouldDehydrateQuery: (query) => {
371
// Only dehydrate posts, not user settings for security
372
return query.queryKey[0] === 'posts'
373
}
374
})
375
}
376
}
377
}
378
```
379
380
### Nested Hydration Boundaries
381
382
```typescript { .api }
383
function Layout({ globalDehydratedState, children }: {
384
globalDehydratedState: DehydratedState
385
children: React.ReactNode
386
}) {
387
return (
388
<HydrationBoundary state={globalDehydratedState}>
389
<Header />
390
<main>{children}</main>
391
<Footer />
392
</HydrationBoundary>
393
)
394
}
395
396
function PostPage({ pageDehydratedState }: { pageDehydratedState: DehydratedState }) {
397
return (
398
<HydrationBoundary state={pageDehydratedState}>
399
<PostContent />
400
<PostComments />
401
</HydrationBoundary>
402
)
403
}
404
```
405
406
## QueryErrorResetBoundary
407
408
**Component for providing error reset functionality to child queries**
409
410
```typescript { .api }
411
const QueryErrorResetBoundary: React.FC<QueryErrorResetBoundaryProps>
412
413
interface QueryErrorResetBoundaryProps {
414
children: QueryErrorResetBoundaryFunction | React.ReactNode
415
}
416
417
type QueryErrorResetBoundaryFunction = (
418
value: QueryErrorResetBoundaryValue,
419
) => React.ReactNode
420
421
interface QueryErrorResetBoundaryValue {
422
clearReset: QueryErrorClearResetFunction
423
isReset: QueryErrorIsResetFunction
424
reset: QueryErrorResetFunction
425
}
426
```
427
428
### Basic Error Boundary
429
430
```typescript { .api }
431
import { QueryErrorResetBoundary } from '@tanstack/react-query'
432
import { ErrorBoundary } from 'react-error-boundary'
433
434
function App() {
435
return (
436
<QueryErrorResetBoundary>
437
{({ reset }) => (
438
<ErrorBoundary
439
onReset={reset}
440
fallbackRender={({ error, resetErrorBoundary }) => (
441
<div className="error-fallback">
442
<h2>Something went wrong:</h2>
443
<pre className="error-message">{error.message}</pre>
444
<button onClick={resetErrorBoundary}>
445
Try again
446
</button>
447
</div>
448
)}
449
>
450
<Suspense fallback={<div>Loading...</div>}>
451
<UserDashboard />
452
</Suspense>
453
</ErrorBoundary>
454
)}
455
</QueryErrorResetBoundary>
456
)
457
}
458
```
459
460
### useQueryErrorResetBoundary
461
462
```typescript { .api }
463
function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue
464
```
465
466
### Custom Error Reset Logic
467
468
```typescript { .api }
469
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
470
471
function CustomErrorBoundary({ children }: { children: React.ReactNode }) {
472
const { reset, isReset } = useQueryErrorResetBoundary()
473
const [hasError, setHasError] = useState(false)
474
475
useEffect(() => {
476
if (isReset()) {
477
setHasError(false)
478
}
479
}, [isReset])
480
481
if (hasError) {
482
return (
483
<div className="error-boundary">
484
<h2>Oops! Something went wrong</h2>
485
<button
486
onClick={() => {
487
reset()
488
setHasError(false)
489
}}
490
>
491
Reset and try again
492
</button>
493
</div>
494
)
495
}
496
497
return (
498
<ErrorBoundary
499
onError={() => setHasError(true)}
500
fallback={null}
501
>
502
{children}
503
</ErrorBoundary>
504
)
505
}
506
```
507
508
### Nested Error Boundaries
509
510
```typescript { .api }
511
function Layout() {
512
return (
513
<QueryErrorResetBoundary>
514
{({ reset }) => (
515
<div>
516
<ErrorBoundary
517
onReset={reset}
518
fallbackRender={({ resetErrorBoundary }) => (
519
<div className="header-error">
520
<span>Header failed to load</span>
521
<button onClick={resetErrorBoundary}>Retry</button>
522
</div>
523
)}
524
>
525
<Header />
526
</ErrorBoundary>
527
528
<main>
529
<ErrorBoundary
530
onReset={reset}
531
fallbackRender={({ resetErrorBoundary }) => (
532
<div className="content-error">
533
<h2>Content failed to load</h2>
534
<button onClick={resetErrorBoundary}>Retry</button>
535
</div>
536
)}
537
>
538
<MainContent />
539
</ErrorBoundary>
540
</main>
541
</div>
542
)}
543
</QueryErrorResetBoundary>
544
)
545
}
546
```
547
548
## IsRestoringProvider
549
550
**Provider component for tracking restoration state**
551
552
```typescript { .api }
553
const IsRestoringProvider: React.Provider<boolean>
554
```
555
556
### useIsRestoring
557
558
```typescript { .api }
559
function useIsRestoring(): boolean
560
```
561
562
### Usage
563
564
```typescript { .api }
565
import { useIsRestoring } from '@tanstack/react-query'
566
567
function GlobalLoadingIndicator() {
568
const isRestoring = useIsRestoring()
569
const isFetching = useIsFetching()
570
571
if (isRestoring) {
572
return (
573
<div className="restoration-indicator">
574
Restoring queries from server...
575
</div>
576
)
577
}
578
579
if (isFetching > 0) {
580
return (
581
<div className="fetching-indicator">
582
Loading... ({isFetching} queries)
583
</div>
584
)
585
}
586
587
return null
588
}
589
```
590
591
### Custom Restoration Handling
592
593
```typescript { .api }
594
function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
595
const [queryClient] = useState(() => new QueryClient())
596
597
return (
598
<QueryClientProvider client={queryClient}>
599
<HydrationBoundary state={dehydratedState}>
600
<IsRestoringProvider value={true}>
601
<RestorationAwareApp />
602
</IsRestoringProvider>
603
</HydrationBoundary>
604
</QueryClientProvider>
605
)
606
}
607
608
function RestorationAwareApp() {
609
const isRestoring = useIsRestoring()
610
611
// Don't render interactive elements during restoration
612
if (isRestoring) {
613
return <SkeletonLoader />
614
}
615
616
return <InteractiveApp />
617
}
618
```
619
620
## Core Management Classes
621
622
The following core classes are available for advanced use cases and custom implementations:
623
624
### QueryCache
625
626
**Manages the cache of queries and their states**
627
628
```typescript { .api }
629
class QueryCache {
630
constructor(config?: QueryCacheConfig)
631
632
add(query: Query): void
633
remove(query: Query): void
634
find(filters: QueryFilters): Query | undefined
635
findAll(filters?: QueryFilters): Query[]
636
notify(event: QueryCacheNotifyEvent): void
637
subscribe(callback: (event: QueryCacheNotifyEvent) => void): () => void
638
clear(): void
639
}
640
641
interface QueryCacheConfig {
642
onError?: (error: unknown, query: Query) => void
643
onSuccess?: (data: unknown, query: Query) => void
644
onSettled?: (data: unknown | undefined, error: unknown | null, query: Query) => void
645
}
646
```
647
648
**Example:**
649
```typescript
650
import { QueryCache } from '@tanstack/react-query'
651
652
const queryCache = new QueryCache({
653
onError: (error, query) => {
654
console.error('Query failed:', query.queryKey, error)
655
},
656
onSuccess: (data, query) => {
657
console.log('Query succeeded:', query.queryKey)
658
}
659
})
660
661
// Use in QueryClient
662
const queryClient = new QueryClient({ queryCache })
663
```
664
665
### MutationCache
666
667
**Manages the cache of mutations and their states**
668
669
```typescript { .api }
670
class MutationCache {
671
constructor(config?: MutationCacheConfig)
672
673
add(mutation: Mutation): void
674
remove(mutation: Mutation): void
675
find(filters: MutationFilters): Mutation | undefined
676
findAll(filters?: MutationFilters): Mutation[]
677
notify(event: MutationCacheNotifyEvent): void
678
subscribe(callback: (event: MutationCacheNotifyEvent) => void): () => void
679
clear(): void
680
}
681
682
interface MutationCacheConfig {
683
onError?: (error: unknown, variables: unknown, context: unknown, mutation: Mutation) => void
684
onSuccess?: (data: unknown, variables: unknown, context: unknown, mutation: Mutation) => void
685
onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown, mutation: Mutation) => void
686
}
687
```
688
689
### Core Managers
690
691
#### focusManager
692
693
**Manages window focus detection for automatic refetching**
694
695
```typescript { .api }
696
const focusManager: {
697
subscribe(callback: (focused: boolean) => void): () => void
698
setFocused(focused?: boolean): void
699
isFocused(): boolean
700
setEventListener(handleFocus: () => void): void
701
}
702
```
703
704
**Example:**
705
```typescript
706
import { focusManager } from '@tanstack/react-query'
707
708
// Custom focus management
709
focusManager.setEventListener(() => {
710
// Custom logic to determine if window is focused
711
const hasFocus = document.hasFocus()
712
focusManager.setFocused(hasFocus)
713
})
714
715
// Subscribe to focus changes
716
const unsubscribe = focusManager.subscribe((focused) => {
717
console.log('Window focus changed:', focused)
718
})
719
```
720
721
#### onlineManager
722
723
**Manages network connectivity detection for automatic refetching**
724
725
```typescript { .api }
726
const onlineManager: {
727
subscribe(callback: (online: boolean) => void): () => void
728
setOnline(online?: boolean): void
729
isOnline(): boolean
730
setEventListener(handleOnline: () => void): void
731
}
732
```
733
734
**Example:**
735
```typescript
736
import { onlineManager } from '@tanstack/react-query'
737
738
// Custom online detection
739
onlineManager.setEventListener(() => {
740
onlineManager.setOnline(navigator.onLine)
741
})
742
743
// Subscribe to online status changes
744
const unsubscribe = onlineManager.subscribe((online) => {
745
if (online) {
746
console.log('Connection restored')
747
} else {
748
console.log('Connection lost')
749
}
750
})
751
```
752
753
#### notifyManager
754
755
**Manages batching and scheduling of notifications**
756
757
```typescript { .api }
758
const notifyManager: {
759
schedule(fn: () => void): void
760
batchCalls<T extends Array<unknown>>(fn: (...args: T) => void): (...args: T) => void
761
flush(): void
762
}
763
```
764
765
### Observer Classes
766
767
Advanced observer classes for custom query management:
768
769
#### QueryObserver
770
771
**Lower-level observer for individual queries**
772
773
```typescript { .api }
774
class QueryObserver<TQueryFnData, TError, TData, TQueryKey extends QueryKey> {
775
constructor(client: QueryClient, options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>)
776
777
subscribe(listener?: (result: QueryObserverResult<TData, TError>) => void): () => void
778
getCurrentResult(): QueryObserverResult<TData, TError>
779
trackResult(result: QueryObserverResult<TData, TError>): QueryObserverResult<TData, TError>
780
getOptimisticResult(options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>): QueryObserverResult<TData, TError>
781
updateResult(): void
782
setOptions(options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>): void
783
destroy(): void
784
}
785
```
786
787
#### InfiniteQueryObserver
788
789
**Observer for infinite/paginated queries**
790
791
```typescript { .api }
792
class InfiniteQueryObserver<TQueryFnData, TError, TData, TQueryKey extends QueryKey, TPageParam> {
793
constructor(client: QueryClient, options: InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>)
794
795
subscribe(listener?: (result: InfiniteQueryObserverResult<TData, TError>) => void): () => void
796
getCurrentResult(): InfiniteQueryObserverResult<TData, TError>
797
// ... similar methods to QueryObserver
798
}
799
```
800
801
#### MutationObserver
802
803
**Observer for individual mutations**
804
805
```typescript { .api }
806
class MutationObserver<TData, TError, TVariables, TContext> {
807
constructor(client: QueryClient, options: MutationObserverOptions<TData, TError, TVariables, TContext>)
808
809
subscribe(listener?: (result: MutationObserverResult<TData, TError, TVariables, TContext>) => void): () => void
810
getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TContext>
811
mutate(variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>): Promise<TData>
812
reset(): void
813
destroy(): void
814
}
815
```
816
817
### Hydration Utilities
818
819
#### dehydrate
820
821
**Serializes QueryClient state for SSR**
822
823
```typescript { .api }
824
function dehydrate(
825
client: QueryClient,
826
options?: DehydrateOptions
827
): DehydratedState
828
829
interface DehydrateOptions {
830
shouldDehydrateMutation?: (mutation: Mutation) => boolean
831
shouldDehydrateQuery?: (query: Query) => boolean
832
}
833
```
834
835
#### hydrate
836
837
**Restores QueryClient state from serialized data**
838
839
```typescript { .api }
840
function hydrate(
841
client: QueryClient,
842
dehydratedState: DehydratedState,
843
options?: HydrateOptions
844
): void
845
846
interface HydrateOptions {
847
defaultOptions?: DefaultOptions
848
}
849
```
850
851
#### defaultShouldDehydrateQuery
852
853
**Default filter for query dehydration**
854
855
```typescript { .api }
856
function defaultShouldDehydrateQuery(query: Query): boolean
857
```
858
859
#### defaultShouldDehydrateMutation
860
861
**Default filter for mutation dehydration**
862
863
```typescript { .api }
864
function defaultShouldDehydrateMutation(mutation: Mutation): boolean
865
```
866
867
**Example:**
868
```typescript
869
import {
870
dehydrate,
871
hydrate,
872
defaultShouldDehydrateQuery,
873
defaultShouldDehydrateMutation
874
} from '@tanstack/react-query'
875
876
// Custom dehydration with selective queries
877
export async function getServerSideProps() {
878
const queryClient = new QueryClient()
879
880
// Prefetch data
881
await queryClient.prefetchQuery({
882
queryKey: ['posts'],
883
queryFn: fetchPosts
884
})
885
886
return {
887
props: {
888
dehydratedState: dehydrate(queryClient, {
889
shouldDehydrateQuery: (query) => {
890
// Use default logic plus custom conditions
891
return defaultShouldDehydrateQuery(query) &&
892
!query.queryKey.includes('sensitive')
893
}
894
})
895
}
896
}
897
}
898
```
899
900
## Best Practices
901
902
### QueryClient Singleton
903
904
```typescript { .api }
905
// ❌ Don't create a new QueryClient on every render
906
function App() {
907
return (
908
<QueryClientProvider client={new QueryClient()}>
909
{/* This creates a new client on every render */}
910
</QueryClientProvider>
911
)
912
}
913
914
// ✅ Create QueryClient outside component or use useState
915
const queryClient = new QueryClient()
916
917
function App() {
918
return (
919
<QueryClientProvider client={queryClient}>
920
{/* QueryClient is stable across renders */}
921
</QueryClientProvider>
922
)
923
}
924
925
// ✅ Or use useState for client-side apps
926
function App() {
927
const [queryClient] = useState(() => new QueryClient())
928
929
return (
930
<QueryClientProvider client={queryClient}>
931
{/* QueryClient is created once and stable */}
932
</QueryClientProvider>
933
)
934
}
935
```
936
937
### Error Boundary Hierarchy
938
939
```typescript { .api }
940
function App() {
941
return (
942
<QueryClientProvider client={queryClient}>
943
{/* Global error boundary for unrecoverable errors */}
944
<QueryErrorResetBoundary>
945
{({ reset }) => (
946
<ErrorBoundary
947
onReset={reset}
948
fallbackRender={GlobalErrorFallback}
949
>
950
<Router>
951
<Routes>
952
<Route path="/*" element={
953
/* Page-level error boundaries for recoverable errors */
954
<QueryErrorResetBoundary>
955
{({ reset }) => (
956
<ErrorBoundary
957
onReset={reset}
958
fallbackRender={PageErrorFallback}
959
>
960
<PageContent />
961
</ErrorBoundary>
962
)}
963
</QueryErrorResetBoundary>
964
} />
965
</Routes>
966
</Router>
967
</ErrorBoundary>
968
)}
969
</QueryErrorResetBoundary>
970
</QueryClientProvider>
971
)
972
}
973
```
974
975
### Context Optimization
976
977
```typescript { .api }
978
// ✅ Use the useQueryClient hook instead of context directly
979
function MyComponent() {
980
const queryClient = useQueryClient()
981
// Hook provides better error messages and type safety
982
}
983
984
// ❌ Don't use context directly unless necessary
985
function MyComponent() {
986
const queryClient = useContext(QueryClientContext)
987
// Manual error checking required
988
if (!queryClient) throw new Error('...')
989
}
990
```
991
992
The provider and context system in React Query offers a robust foundation for managing query state, error handling, and SSR hydration across your entire React application.