or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-integration.mdcache-management.mdclient-management.mdhydration.mdindex.mdinfinite-queries.mdmutations.mdquery-observers.mdquery-operations.mdutilities.md

hydration.mddocs/

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

```