0
# SSR & Hydration
1
2
Server-side rendering support with state dehydration and hydration for seamless SSR/SSG integration. React Query provides utilities to serialize server-side data and rehydrate it on the client.
3
4
## Capabilities
5
6
### useHydrate Hook
7
8
Hook for hydrating QueryClient with server-side rendered state.
9
10
```typescript { .api }
11
/**
12
* Hydrate QueryClient with server-side state
13
* @param state - Dehydrated state from server
14
* @param options - Hydration configuration options
15
*/
16
function useHydrate(
17
state: unknown,
18
options?: HydrateOptions & ContextOptions
19
): void;
20
21
interface HydrateOptions {
22
/** Function to determine which queries should be hydrated */
23
shouldDehydrateQuery?: ShouldDehydrateQueryFunction;
24
/** Function to determine which mutations should be hydrated */
25
shouldDehydrateMutation?: ShouldDehydrateMutationFunction;
26
/** Default options for hydrated queries */
27
defaultOptions?: {
28
queries?: QueryOptions;
29
mutations?: MutationOptions;
30
};
31
}
32
33
interface ContextOptions {
34
/** Custom React context to use */
35
context?: React.Context<QueryClient | undefined>;
36
}
37
38
type ShouldDehydrateQueryFunction = (query: Query) => boolean;
39
type ShouldDehydrateMutationFunction = (mutation: Mutation) => boolean;
40
```
41
42
**Usage Examples:**
43
44
```typescript
45
import { useHydrate } from "react-query";
46
47
// Basic hydration
48
function App({ dehydratedState }: { dehydratedState: any }) {
49
useHydrate(dehydratedState);
50
51
return (
52
<div>
53
<UserProfile />
54
<PostsList />
55
</div>
56
);
57
}
58
59
// Hydration with options
60
function AppWithOptions({ dehydratedState }: { dehydratedState: any }) {
61
useHydrate(dehydratedState, {
62
defaultOptions: {
63
queries: {
64
staleTime: 60 * 1000 // 1 minute
65
}
66
}
67
});
68
69
return <MainContent />;
70
}
71
72
// Conditional hydration
73
function ConditionalHydration({ dehydratedState, shouldHydrate }: {
74
dehydratedState: any;
75
shouldHydrate: boolean;
76
}) {
77
useHydrate(shouldHydrate ? dehydratedState : undefined);
78
79
return <Content />;
80
}
81
```
82
83
### Hydrate Component
84
85
Component wrapper for hydrating server-side state.
86
87
```typescript { .api }
88
/**
89
* Component that hydrates QueryClient with server-side state
90
* @param props - Hydration props with state and options
91
* @returns Children with hydrated state
92
*/
93
function Hydrate(props: HydrateProps): React.ReactElement;
94
95
interface HydrateProps {
96
/** Dehydrated state from server */
97
state?: unknown;
98
/** Hydration configuration options */
99
options?: HydrateOptions;
100
/** Child components to render */
101
children?: React.ReactNode;
102
}
103
```
104
105
**Usage Examples:**
106
107
```typescript
108
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
109
110
// Basic component usage
111
function App({ pageProps }: { pageProps: any }) {
112
const queryClient = new QueryClient();
113
114
return (
115
<QueryClientProvider client={queryClient}>
116
<Hydrate state={pageProps.dehydratedState}>
117
<MyApp {...pageProps} />
118
</Hydrate>
119
</QueryClientProvider>
120
);
121
}
122
123
// Next.js integration
124
function MyApp({ Component, pageProps }: AppProps) {
125
const [queryClient] = useState(() => new QueryClient());
126
127
return (
128
<QueryClientProvider client={queryClient}>
129
<Hydrate state={pageProps.dehydratedState}>
130
<Component {...pageProps} />
131
</Hydrate>
132
</QueryClientProvider>
133
);
134
}
135
136
// Multiple hydration levels
137
function NestedHydration({
138
globalState,
139
pageState,
140
children
141
}: {
142
globalState: any;
143
pageState: any;
144
children: React.ReactNode;
145
}) {
146
return (
147
<Hydrate state={globalState}>
148
<div>
149
<GlobalNav />
150
<Hydrate state={pageState}>
151
{children}
152
</Hydrate>
153
</div>
154
</Hydrate>
155
);
156
}
157
```
158
159
### IsRestoringProvider Component
160
161
Context provider for tracking restoration state during SSR hydration.
162
163
```typescript { .api }
164
/**
165
* Provider for restoration state context
166
* Used internally to track hydration state
167
*/
168
const IsRestoringProvider: React.Provider<boolean>;
169
170
/**
171
* Hook to check if app is currently restoring from SSR
172
* @returns Boolean indicating if restoration is in progress
173
*/
174
function useIsRestoring(): boolean;
175
```
176
177
**Usage Examples:**
178
179
```typescript
180
import { useIsRestoring, IsRestoringProvider } from "react-query";
181
182
// Check restoration state
183
function MyComponent() {
184
const isRestoring = useIsRestoring();
185
186
if (isRestoring) {
187
return <div>Restoring from server...</div>;
188
}
189
190
return <div>Client-side rendering active</div>;
191
}
192
193
// Custom restoration provider (rarely needed)
194
function CustomRestorationProvider({ children }: { children: React.ReactNode }) {
195
const [isRestoring, setIsRestoring] = useState(true);
196
197
useEffect(() => {
198
// Custom logic to determine when restoration is complete
199
const timer = setTimeout(() => setIsRestoring(false), 100);
200
return () => clearTimeout(timer);
201
}, []);
202
203
return (
204
<IsRestoringProvider value={isRestoring}>
205
{children}
206
</IsRestoringProvider>
207
);
208
}
209
```
210
211
### Core Hydration Functions
212
213
Low-level functions for dehydrating and hydrating QueryClient state.
214
215
```typescript { .api }
216
/**
217
* Serialize QueryClient state for server-side rendering
218
* @param client - QueryClient instance to dehydrate
219
* @param options - Dehydration configuration
220
* @returns Serializable state object
221
*/
222
function dehydrate(
223
client: QueryClient,
224
options?: DehydrateOptions
225
): DehydratedState;
226
227
/**
228
* Restore QueryClient state from serialized data
229
* @param client - QueryClient instance to hydrate
230
* @param dehydratedState - Serialized state from dehydrate
231
* @param options - Hydration configuration
232
*/
233
function hydrate(
234
client: QueryClient,
235
dehydratedState: unknown,
236
options?: HydrateOptions
237
): void;
238
239
interface DehydrateOptions {
240
/** Function to determine which queries should be dehydrated */
241
shouldDehydrateQuery?: ShouldDehydrateQueryFunction;
242
/** Function to determine which mutations should be dehydrated */
243
shouldDehydrateMutation?: ShouldDehydrateMutationFunction;
244
}
245
246
interface DehydratedState {
247
/** Serialized queries */
248
queries: Array<{
249
queryKey: QueryKey;
250
queryHash: string;
251
state: QueryState;
252
}>;
253
/** Serialized mutations */
254
mutations: Array<{
255
mutationKey?: MutationKey;
256
state: MutationState;
257
}>;
258
}
259
```
260
261
## Advanced Usage Patterns
262
263
### Next.js Integration
264
265
Complete Next.js SSR/SSG setup with React Query:
266
267
```typescript
268
// pages/_app.tsx
269
import { useState } from 'react';
270
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
271
import { ReactQueryDevtools } from 'react-query/devtools';
272
import type { AppProps } from 'next/app';
273
274
export default function MyApp({ Component, pageProps }: AppProps) {
275
const [queryClient] = useState(() => new QueryClient({
276
defaultOptions: {
277
queries: {
278
staleTime: 60 * 1000, // 1 minute
279
refetchOnWindowFocus: false,
280
},
281
},
282
}));
283
284
return (
285
<QueryClientProvider client={queryClient}>
286
<Hydrate state={pageProps.dehydratedState}>
287
<Component {...pageProps} />
288
</Hydrate>
289
<ReactQueryDevtools initialIsOpen={false} />
290
</QueryClientProvider>
291
);
292
}
293
294
// pages/users/[id].tsx
295
import { GetServerSideProps } from 'next';
296
import { QueryClient, dehydrate } from 'react-query';
297
298
export default function UserPage({ userId }: { userId: string }) {
299
const { data: user } = useQuery({
300
queryKey: ['user', userId],
301
queryFn: () => fetchUser(userId)
302
});
303
304
const { data: posts } = useQuery({
305
queryKey: ['posts', userId],
306
queryFn: () => fetchUserPosts(userId)
307
});
308
309
return (
310
<div>
311
<h1>{user?.name}</h1>
312
<PostsList posts={posts} />
313
</div>
314
);
315
}
316
317
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
318
const queryClient = new QueryClient();
319
const userId = params?.id as string;
320
321
// Prefetch data on server
322
await queryClient.prefetchQuery({
323
queryKey: ['user', userId],
324
queryFn: () => fetchUser(userId)
325
});
326
327
await queryClient.prefetchQuery({
328
queryKey: ['posts', userId],
329
queryFn: () => fetchUserPosts(userId)
330
});
331
332
return {
333
props: {
334
dehydratedState: dehydrate(queryClient),
335
userId
336
}
337
};
338
};
339
```
340
341
### Selective Hydration
342
343
Only hydrating specific queries to reduce bundle size:
344
345
```typescript
346
// Server-side dehydration with filtering
347
export const getServerSideProps: GetServerSideProps = async () => {
348
const queryClient = new QueryClient();
349
350
// Prefetch multiple queries
351
await Promise.all([
352
queryClient.prefetchQuery({
353
queryKey: ['user', 'current'],
354
queryFn: fetchCurrentUser
355
}),
356
queryClient.prefetchQuery({
357
queryKey: ['posts', 'popular'],
358
queryFn: fetchPopularPosts
359
}),
360
queryClient.prefetchQuery({
361
queryKey: ['analytics', 'daily'],
362
queryFn: fetchDailyAnalytics
363
})
364
]);
365
366
return {
367
props: {
368
dehydratedState: dehydrate(queryClient, {
369
shouldDehydrateQuery: (query) => {
370
// Only dehydrate user and posts, not analytics
371
const queryKey = query.queryKey[0] as string;
372
return ['user', 'posts'].includes(queryKey);
373
}
374
})
375
}
376
};
377
};
378
379
// Client-side selective hydration
380
function App({ dehydratedState }: { dehydratedState: any }) {
381
useHydrate(dehydratedState, {
382
shouldDehydrateQuery: (query) => {
383
// Only hydrate queries that are not stale
384
return query.state.dataUpdatedAt > Date.now() - 60000; // 1 minute
385
}
386
});
387
388
return <MainContent />;
389
}
390
```
391
392
### Progressive Hydration
393
394
Hydrating data progressively as components mount:
395
396
```typescript
397
function ProgressiveHydrationApp({ dehydratedState }: { dehydratedState: any }) {
398
const [hydratedSections, setHydratedSections] = useState<string[]>([]);
399
400
const hydrateSection = (section: string) => {
401
if (!hydratedSections.includes(section)) {
402
setHydratedSections(prev => [...prev, section]);
403
}
404
};
405
406
return (
407
<div>
408
{/* Always hydrate critical data */}
409
<Hydrate state={dehydratedState.critical}>
410
<Header />
411
</Hydrate>
412
413
{/* Progressively hydrate sections */}
414
<InView onChange={(inView) => inView && hydrateSection('main')}>
415
{hydratedSections.includes('main') ? (
416
<Hydrate state={dehydratedState.main}>
417
<MainContent />
418
</Hydrate>
419
) : (
420
<div>Loading main content...</div>
421
)}
422
</InView>
423
424
<InView onChange={(inView) => inView && hydrateSection('sidebar')}>
425
{hydratedSections.includes('sidebar') ? (
426
<Hydrate state={dehydratedState.sidebar}>
427
<Sidebar />
428
</Hydrate>
429
) : (
430
<div>Loading sidebar...</div>
431
)}
432
</InView>
433
</div>
434
);
435
}
436
```
437
438
### Error Handling in SSR
439
440
Handling errors during server-side prefetching:
441
442
```typescript
443
// Robust server-side prefetching
444
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
445
const queryClient = new QueryClient({
446
defaultOptions: {
447
queries: {
448
retry: false, // Don't retry on server
449
staleTime: Infinity, // Keep data fresh until client hydration
450
},
451
},
452
});
453
454
const userId = params?.id as string;
455
456
try {
457
// Prefetch critical data
458
await queryClient.prefetchQuery({
459
queryKey: ['user', userId],
460
queryFn: () => fetchUser(userId)
461
});
462
463
// Prefetch optional data (don't fail if this errors)
464
await queryClient.prefetchQuery({
465
queryKey: ['posts', userId],
466
queryFn: () => fetchUserPosts(userId)
467
}).catch(error => {
468
console.warn('Failed to prefetch posts:', error);
469
});
470
471
} catch (error) {
472
console.error('Failed to prefetch user:', error);
473
474
// Return error page or redirect
475
return {
476
notFound: true,
477
};
478
}
479
480
return {
481
props: {
482
dehydratedState: dehydrate(queryClient),
483
userId
484
}
485
};
486
};
487
488
// Client-side error handling during hydration
489
function ErrorBoundaryWithHydration({
490
children,
491
dehydratedState
492
}: {
493
children: React.ReactNode;
494
dehydratedState: any;
495
}) {
496
const [hydrationError, setHydrationError] = useState<Error | null>(null);
497
498
useEffect(() => {
499
try {
500
// Attempt hydration
501
useHydrate(dehydratedState);
502
} catch (error) {
503
setHydrationError(error as Error);
504
}
505
}, [dehydratedState]);
506
507
if (hydrationError) {
508
return (
509
<div>
510
<h2>Hydration Error</h2>
511
<p>Failed to restore server state. Falling back to client-side fetching.</p>
512
<button onClick={() => setHydrationError(null)}>
513
Try Again
514
</button>
515
</div>
516
);
517
}
518
519
return <>{children}</>;
520
}
521
```
522
523
### IsRestoringProvider and useIsRestoring
524
525
Provider and hook for tracking SSR hydration restoration state during client-side hydration.
526
527
```typescript { .api }
528
/**
529
* Provider component for managing restoration state during SSR hydration
530
* @param value - Boolean indicating if restoration is in progress
531
* @param children - Child components
532
*/
533
function IsRestoringProvider(props: {
534
value: boolean;
535
children: React.ReactNode;
536
}): React.ReactElement;
537
538
/**
539
* Hook to check if queries are currently being restored during SSR hydration
540
* @returns Boolean indicating if restoration is in progress
541
*/
542
function useIsRestoring(): boolean;
543
```
544
545
**Usage Examples:**
546
547
```typescript
548
import { IsRestoringProvider, useIsRestoring } from "react-query";
549
550
// App-level restoration tracking
551
function App({ children }: { children: React.ReactNode }) {
552
const [isRestoring, setIsRestoring] = useState(true);
553
554
useEffect(() => {
555
// Set restoration complete after hydration
556
const timer = setTimeout(() => setIsRestoring(false), 100);
557
return () => clearTimeout(timer);
558
}, []);
559
560
return (
561
<IsRestoringProvider value={isRestoring}>
562
<div className={isRestoring ? 'restoring' : 'restored'}>
563
{children}
564
</div>
565
</IsRestoringProvider>
566
);
567
}
568
569
// Component that adapts behavior during restoration
570
function DataComponent() {
571
const isRestoring = useIsRestoring();
572
const { data, isLoading } = useQuery({
573
queryKey: ['data'],
574
queryFn: fetchData
575
});
576
577
// Don't show loading state during restoration to prevent flash
578
if (isRestoring) {
579
return <div className="skeleton">Loading...</div>;
580
}
581
582
if (isLoading) {
583
return <div className="spinner">Fetching data...</div>;
584
}
585
586
return <div>{JSON.stringify(data)}</div>;
587
}
588
589
// Conditional rendering based on restoration state
590
function OptimizedComponent() {
591
const isRestoring = useIsRestoring();
592
593
return (
594
<div>
595
{isRestoring ? (
596
// Show static content during restoration
597
<div className="static-content">
598
<h1>Welcome</h1>
599
<p>Loading your personalized content...</p>
600
</div>
601
) : (
602
// Show dynamic content after restoration
603
<DynamicContent />
604
)}
605
</div>
606
);
607
}
608
609
// SSR-aware loading states
610
function SmartLoadingComponent() {
611
const isRestoring = useIsRestoring();
612
const { data, isLoading, isFetching } = useQuery({
613
queryKey: ['smart-data'],
614
queryFn: fetchSmartData
615
});
616
617
// During restoration, rely on server-rendered content
618
if (isRestoring) {
619
return data ? (
620
<div className="hydrated-content">{data.content}</div>
621
) : (
622
<div className="placeholder-content">Content loading...</div>
623
);
624
}
625
626
// After restoration, show normal loading states
627
if (isLoading) {
628
return <div className="loading-spinner">Loading...</div>;
629
}
630
631
if (isFetching) {
632
return (
633
<div className="refreshing">
634
<div className="content">{data?.content}</div>
635
<div className="refresh-indicator">Updating...</div>
636
</div>
637
);
638
}
639
640
return <div className="content">{data?.content}</div>;
641
}
642
```
643
644
### Custom Hydration Logic
645
646
Implementing custom hydration strategies:
647
648
```typescript
649
function useCustomHydration(dehydratedState: any) {
650
const queryClient = useQueryClient();
651
const [isHydrated, setIsHydrated] = useState(false);
652
653
useEffect(() => {
654
if (!dehydratedState || isHydrated) return;
655
656
// Custom hydration logic
657
const hydrateData = async () => {
658
try {
659
// Validate dehydrated state
660
if (!isValidDehydratedState(dehydratedState)) {
661
console.warn('Invalid dehydrated state, skipping hydration');
662
return;
663
}
664
665
// Transform data before hydration if needed
666
const transformedState = transformDehydratedState(dehydratedState);
667
668
// Hydrate with custom options
669
hydrate(queryClient, transformedState, {
670
defaultOptions: {
671
queries: {
672
staleTime: 30 * 1000, // 30 seconds
673
cacheTime: 5 * 60 * 1000, // 5 minutes
674
}
675
}
676
});
677
678
setIsHydrated(true);
679
} catch (error) {
680
console.error('Custom hydration failed:', error);
681
}
682
};
683
684
hydrateData();
685
}, [dehydratedState, queryClient, isHydrated]);
686
687
return isHydrated;
688
}
689
690
function isValidDehydratedState(state: any): boolean {
691
return (
692
state &&
693
typeof state === 'object' &&
694
Array.isArray(state.queries) &&
695
Array.isArray(state.mutations)
696
);
697
}
698
699
function transformDehydratedState(state: any): any {
700
// Transform or filter the state as needed
701
return {
702
...state,
703
queries: state.queries.filter((query: any) => {
704
// Only hydrate recent queries
705
return query.state.dataUpdatedAt > Date.now() - 60000;
706
})
707
};
708
}
709
```