0
# Hydration & Serialization
1
2
Server-side rendering support with serialization and deserialization of client state for seamless hydration across client-server boundaries and persistent cache storage.
3
4
## Capabilities
5
6
### Dehydration
7
8
Serialize QueryClient state for transfer or storage.
9
10
```typescript { .api }
11
/**
12
* Serialize QueryClient state to a transferable format
13
* Converts queries and mutations to JSON-serializable format
14
* @param client - QueryClient instance to dehydrate
15
* @param options - Options controlling what gets dehydrated
16
* @returns Serializable state object
17
*/
18
function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
19
20
interface DehydrateOptions {
21
/**
22
* Function to determine which queries should be dehydrated
23
* @param query - Query to evaluate for dehydration
24
* @returns true if query should be included in dehydrated state
25
*/
26
shouldDehydrateQuery?: (query: Query) => boolean;
27
28
/**
29
* Function to determine which mutations should be dehydrated
30
* @param mutation - Mutation to evaluate for dehydration
31
* @returns true if mutation should be included in dehydrated state
32
*/
33
shouldDehydrateMutation?: (mutation: Mutation) => boolean;
34
35
/**
36
* Function to serialize data values
37
* @param data - Data to serialize
38
* @returns Serialized data
39
*/
40
serializeData?: (data: unknown) => unknown;
41
}
42
43
interface DehydratedState {
44
/** Serialized mutations */
45
mutations: Array<DehydratedMutation>;
46
47
/** Serialized queries */
48
queries: Array<DehydratedQuery>;
49
}
50
51
interface DehydratedQuery {
52
queryHash: string;
53
queryKey: QueryKey;
54
state: QueryState;
55
}
56
57
interface DehydratedMutation {
58
mutationKey?: MutationKey;
59
state: MutationState;
60
}
61
```
62
63
**Usage Examples:**
64
65
```typescript
66
import { QueryClient, dehydrate } from "@tanstack/query-core";
67
68
const queryClient = new QueryClient();
69
70
// Basic dehydration
71
const dehydratedState = dehydrate(queryClient);
72
73
// Dehydration with custom filters
74
const selectiveDehydratedState = dehydrate(queryClient, {
75
shouldDehydrateQuery: (query) => {
76
// Only dehydrate successful queries that are not stale
77
return query.state.status === 'success' && !query.isStale();
78
},
79
shouldDehydrateMutation: (mutation) => {
80
// Only dehydrate pending mutations
81
return mutation.state.status === 'pending';
82
},
83
});
84
85
// Dehydration with data transformation
86
const transformedDehydratedState = dehydrate(queryClient, {
87
shouldDehydrateQuery: (query) => {
88
// Skip large data sets in SSR
89
const dataSize = JSON.stringify(query.state.data).length;
90
return dataSize < 10000; // Less than 10KB
91
},
92
serializeData: (data) => {
93
// Custom serialization (e.g., handle Dates, BigInts)
94
return JSON.parse(JSON.stringify(data, (key, value) => {
95
if (value instanceof Date) {
96
return { __type: 'Date', value: value.toISOString() };
97
}
98
if (typeof value === 'bigint') {
99
return { __type: 'BigInt', value: value.toString() };
100
}
101
return value;
102
}));
103
},
104
});
105
106
// Convert to JSON for transfer
107
const serialized = JSON.stringify(dehydratedState);
108
109
// Store in localStorage
110
localStorage.setItem('react-query-cache', serialized);
111
112
// Send to client in SSR
113
const html = `
114
<script>
115
window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)};
116
</script>
117
`;
118
```
119
120
### Hydration
121
122
Restore QueryClient state from dehydrated data.
123
124
```typescript { .api }
125
/**
126
* Restore QueryClient state from dehydrated data
127
* Recreates queries and mutations from serialized state
128
* @param client - QueryClient instance to hydrate
129
* @param dehydratedState - Previously dehydrated state
130
* @param options - Options controlling hydration behavior
131
*/
132
function hydrate(
133
client: QueryClient,
134
dehydratedState: unknown,
135
options?: HydrateOptions
136
): void;
137
138
interface HydrateOptions {
139
/**
140
* Default options to apply to hydrated queries
141
*/
142
defaultOptions?: {
143
queries?: Partial<QueryObserverOptions>;
144
mutations?: Partial<MutationObserverOptions>;
145
};
146
147
/**
148
* Function to deserialize data values
149
* @param data - Data to deserialize
150
* @returns Deserialized data
151
*/
152
deserializeData?: (data: unknown) => unknown;
153
}
154
```
155
156
**Usage Examples:**
157
158
```typescript
159
import { QueryClient, hydrate } from "@tanstack/query-core";
160
161
// Client-side hydration
162
const queryClient = new QueryClient();
163
164
// Basic hydration from window object (SSR)
165
if (typeof window !== 'undefined' && window.__REACT_QUERY_STATE__) {
166
hydrate(queryClient, window.__REACT_QUERY_STATE__);
167
}
168
169
// Hydration from localStorage
170
const storedState = localStorage.getItem('react-query-cache');
171
if (storedState) {
172
try {
173
const dehydratedState = JSON.parse(storedState);
174
hydrate(queryClient, dehydratedState);
175
} catch (error) {
176
console.error('Failed to hydrate from localStorage:', error);
177
localStorage.removeItem('react-query-cache');
178
}
179
}
180
181
// Hydration with custom deserialization
182
hydrate(queryClient, dehydratedState, {
183
deserializeData: (data) => {
184
// Custom deserialization to handle special types
185
return JSON.parse(JSON.stringify(data), (key, value) => {
186
if (value && typeof value === 'object') {
187
if (value.__type === 'Date') {
188
return new Date(value.value);
189
}
190
if (value.__type === 'BigInt') {
191
return BigInt(value.value);
192
}
193
}
194
return value;
195
});
196
},
197
defaultOptions: {
198
queries: {
199
staleTime: 1000 * 60 * 5, // 5 minutes
200
},
201
},
202
});
203
```
204
205
### Default Dehydration Functions
206
207
Built-in functions for common dehydration scenarios.
208
209
```typescript { .api }
210
/**
211
* Default function to determine if a query should be dehydrated
212
* Only dehydrates successful queries that are not infinite queries
213
* @param query - Query to evaluate
214
* @returns true if query should be dehydrated
215
*/
216
function defaultShouldDehydrateQuery(query: Query): boolean;
217
218
/**
219
* Default function to determine if a mutation should be dehydrated
220
* Only dehydrates pending mutations
221
* @param mutation - Mutation to evaluate
222
* @returns true if mutation should be dehydrated
223
*/
224
function defaultShouldDehydrateMutation(mutation: Mutation): boolean;
225
```
226
227
**Usage Examples:**
228
229
```typescript
230
import {
231
dehydrate,
232
defaultShouldDehydrateQuery,
233
defaultShouldDehydrateMutation
234
} from "@tanstack/query-core";
235
236
// Using default functions with custom logic
237
const dehydratedState = dehydrate(queryClient, {
238
shouldDehydrateQuery: (query) => {
239
// Use default logic but exclude certain query keys
240
if (!defaultShouldDehydrateQuery(query)) {
241
return false;
242
}
243
244
// Don't dehydrate sensitive data
245
const sensitiveKeys = ['user-secrets', 'api-keys'];
246
return !sensitiveKeys.some(key =>
247
JSON.stringify(query.queryKey).includes(key)
248
);
249
},
250
shouldDehydrateMutation: defaultShouldDehydrateMutation,
251
});
252
```
253
254
### SSR Integration Patterns
255
256
Common patterns for server-side rendering integration.
257
258
```typescript { .api }
259
// Server-side (Next.js getServerSideProps example)
260
export async function getServerSideProps() {
261
const queryClient = new QueryClient();
262
263
// Prefetch data on server
264
await queryClient.prefetchQuery({
265
queryKey: ['user', userId],
266
queryFn: () => fetchUser(userId),
267
});
268
269
await queryClient.prefetchQuery({
270
queryKey: ['posts'],
271
queryFn: () => fetchPosts(),
272
});
273
274
// Dehydrate state
275
const dehydratedState = dehydrate(queryClient, {
276
shouldDehydrateQuery: (query) => {
277
// Only send successful queries to client
278
return query.state.status === 'success';
279
},
280
});
281
282
return {
283
props: {
284
dehydratedState,
285
},
286
};
287
}
288
289
// Client-side component
290
function MyApp({ dehydratedState }) {
291
const [queryClient] = useState(() => new QueryClient());
292
293
// Hydrate on client mount
294
useEffect(() => {
295
if (dehydratedState) {
296
hydrate(queryClient, dehydratedState);
297
}
298
}, [queryClient, dehydratedState]);
299
300
return (
301
<QueryClientProvider client={queryClient}>
302
<App />
303
</QueryClientProvider>
304
);
305
}
306
```
307
308
### Persistent Cache Implementation
309
310
Implementing persistent caching with automatic dehydration/hydration.
311
312
```typescript { .api }
313
class PersistentQueryClient {
314
private queryClient: QueryClient;
315
private persistKey: string;
316
317
constructor(persistKey = 'react-query-cache') {
318
this.persistKey = persistKey;
319
this.queryClient = new QueryClient({
320
defaultOptions: {
321
queries: {
322
gcTime: 1000 * 60 * 60 * 24, // 24 hours
323
},
324
},
325
});
326
327
this.loadFromStorage();
328
this.setupAutoPersist();
329
}
330
331
private loadFromStorage() {
332
try {
333
const stored = localStorage.getItem(this.persistKey);
334
if (stored) {
335
const dehydratedState = JSON.parse(stored);
336
hydrate(this.queryClient, dehydratedState);
337
}
338
} catch (error) {
339
console.error('Failed to load persisted cache:', error);
340
localStorage.removeItem(this.persistKey);
341
}
342
}
343
344
private setupAutoPersist() {
345
// Persist cache periodically
346
setInterval(() => {
347
this.persistToStorage();
348
}, 30000); // Every 30 seconds
349
350
// Persist on page unload
351
window.addEventListener('beforeunload', () => {
352
this.persistToStorage();
353
});
354
}
355
356
private persistToStorage() {
357
try {
358
const dehydratedState = dehydrate(this.queryClient, {
359
shouldDehydrateQuery: (query) => {
360
// Only persist successful, non-stale queries
361
return query.state.status === 'success' &&
362
!query.isStale() &&
363
query.state.dataUpdatedAt > Date.now() - (1000 * 60 * 60); // Less than 1 hour old
364
},
365
});
366
367
localStorage.setItem(this.persistKey, JSON.stringify(dehydratedState));
368
} catch (error) {
369
console.error('Failed to persist cache:', error);
370
}
371
}
372
373
getClient() {
374
return this.queryClient;
375
}
376
377
clearPersisted() {
378
localStorage.removeItem(this.persistKey);
379
}
380
}
381
382
// Usage
383
const persistentClient = new PersistentQueryClient();
384
const queryClient = persistentClient.getClient();
385
```
386
387
### Cache Versioning and Migration
388
389
Handling cache version changes and data migration.
390
391
```typescript { .api }
392
interface VersionedDehydratedState extends DehydratedState {
393
version: string;
394
timestamp: number;
395
}
396
397
class VersionedCache {
398
private static CURRENT_VERSION = '1.2.0';
399
private static MAX_AGE = 1000 * 60 * 60 * 24 * 7; // 7 days
400
401
static dehydrate(client: QueryClient): VersionedDehydratedState {
402
const baseState = dehydrate(client);
403
return {
404
...baseState,
405
version: this.CURRENT_VERSION,
406
timestamp: Date.now(),
407
};
408
}
409
410
static hydrate(client: QueryClient, state: unknown): boolean {
411
if (!this.isValidState(state)) {
412
console.warn('Invalid or outdated cache state, skipping hydration');
413
return false;
414
}
415
416
const versionedState = state as VersionedDehydratedState;
417
418
// Migrate if needed
419
const migratedState = this.migrate(versionedState);
420
421
hydrate(client, migratedState);
422
return true;
423
}
424
425
private static isValidState(state: unknown): state is VersionedDehydratedState {
426
if (!state || typeof state !== 'object') return false;
427
428
const versionedState = state as VersionedDehydratedState;
429
430
// Check version compatibility
431
if (!versionedState.version || !this.isCompatibleVersion(versionedState.version)) {
432
return false;
433
}
434
435
// Check age
436
if (!versionedState.timestamp || Date.now() - versionedState.timestamp > this.MAX_AGE) {
437
return false;
438
}
439
440
return true;
441
}
442
443
private static isCompatibleVersion(version: string): boolean {
444
// Simple major version check
445
const [currentMajor] = this.CURRENT_VERSION.split('.');
446
const [stateMajor] = version.split('.');
447
return currentMajor === stateMajor;
448
}
449
450
private static migrate(state: VersionedDehydratedState): DehydratedState {
451
// Implement migration logic based on version
452
if (state.version === '1.1.0') {
453
// Example migration from 1.1.0 to 1.2.0
454
return {
455
...state,
456
queries: state.queries.map(query => ({
457
...query,
458
// Add new fields or transform existing ones
459
})),
460
};
461
}
462
463
return state;
464
}
465
}
466
467
// Usage
468
const dehydratedState = VersionedCache.dehydrate(queryClient);
469
localStorage.setItem('cache', JSON.stringify(dehydratedState));
470
471
// Later...
472
const storedState = localStorage.getItem('cache');
473
if (storedState) {
474
const success = VersionedCache.hydrate(queryClient, JSON.parse(storedState));
475
if (!success) {
476
// Handle failed hydration (clear cache, show message, etc.)
477
localStorage.removeItem('cache');
478
}
479
}
480
```
481
482
## Core Types
483
484
```typescript { .api }
485
interface QueryState<TData = unknown, TError = Error> {
486
data: TData | undefined;
487
dataUpdateCount: number;
488
dataUpdatedAt: number;
489
error: TError | null;
490
errorUpdateCount: number;
491
errorUpdatedAt: number;
492
fetchFailureCount: number;
493
fetchFailureReason: TError | null;
494
fetchMeta: FetchMeta | null;
495
isInvalidated: boolean;
496
status: QueryStatus;
497
fetchStatus: FetchStatus;
498
}
499
500
interface MutationState<TData = unknown, TError = Error, TVariables = void, TContext = unknown> {
501
context: TContext | undefined;
502
data: TData | undefined;
503
error: TError | null;
504
failureCount: number;
505
failureReason: TError | null;
506
isPaused: boolean;
507
status: MutationStatus;
508
variables: TVariables | undefined;
509
submittedAt: number;
510
}
511
512
type QueryStatus = 'pending' | 'error' | 'success';
513
type FetchStatus = 'fetching' | 'paused' | 'idle';
514
type MutationStatus = 'idle' | 'pending' | 'success' | 'error';
515
516
interface FetchMeta extends Record<string, unknown> {}
517
```