or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

client-persistence.mdindex.mdquery-persistence.mdretry-strategies.md

query-persistence.mddocs/

0

# Fine-Grained Query Persistence

1

2

**Warning: Experimental feature** - The fine-grained query persistence API is experimental and may change in future versions.

3

4

This capability enables query-by-query persistence with selective restoration and advanced storage management. Unlike client-level persistence which saves entire cache state, this approach allows granular control over which queries are persisted and restored.

5

6

## Capabilities

7

8

### Create Query Persister

9

10

Factory function that creates a persister object with methods for managing individual query persistence.

11

12

```typescript { .api }

13

/**

14

* Creates fine-grained query persistence functionality (experimental feature)

15

* @param options - Configuration options for the persister

16

* @returns Persister object with query management methods

17

*/

18

function experimental_createQueryPersister<TStorageValue = string>(

19

options: StoragePersisterOptions<TStorageValue>

20

): QueryPersisterObject;

21

22

interface QueryPersisterObject {

23

persisterFn: <T, TQueryKey extends QueryKey>(

24

queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,

25

ctx: QueryFunctionContext<TQueryKey>,

26

query: Query

27

) => Promise<T>;

28

persistQuery: (query: Query) => Promise<void>;

29

persistQueryByKey: (queryKey: QueryKey, queryClient: QueryClient) => Promise<void>;

30

retrieveQuery: <T>(

31

queryHash: string,

32

afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void

33

) => Promise<T | undefined>;

34

persisterGc: () => Promise<void>;

35

restoreQueries: (

36

queryClient: QueryClient,

37

filters?: Pick<QueryFilters, 'queryKey' | 'exact'>

38

) => Promise<void>;

39

}

40

```

41

42

**Usage Example:**

43

44

```typescript

45

import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core";

46

import { useQuery } from "@tanstack/react-query";

47

48

// Create persister with localStorage

49

const persister = experimental_createQueryPersister({

50

storage: localStorage,

51

buster: "app-v1.0",

52

maxAge: 1000 * 60 * 60 * 6, // 6 hours

53

prefix: "my-app-cache",

54

filters: {

55

predicate: (query) => {

56

// Only persist successful queries with specific keys

57

return query.state.status === 'success' &&

58

query.queryKey[0] === 'user-data';

59

}

60

}

61

});

62

63

// Use with individual queries

64

function UserProfile({ userId }) {

65

return useQuery({

66

queryKey: ['user-data', userId],

67

queryFn: ({ queryKey }) => fetchUser(queryKey[1]),

68

persister: persister.persisterFn,

69

});

70

}

71

72

// Manual query operations

73

await persister.persistQueryByKey(['user-data', '123'], queryClient);

74

await persister.restoreQueries(queryClient, { queryKey: ['user-data'] });

75

await persister.persisterGc(); // Clean up expired entries

76

```

77

78

### Storage Configuration

79

80

Configure how data is stored and retrieved from the storage backend.

81

82

```typescript { .api }

83

interface StoragePersisterOptions<TStorageValue = string> {

84

/** The storage client used for setting and retrieving items from cache.

85

* For SSR pass in `undefined`. */

86

storage: AsyncStorage<TStorageValue> | undefined | null;

87

/**

88

* How to serialize the data to storage.

89

* @default `JSON.stringify`

90

*/

91

serialize?: (persistedQuery: PersistedQuery) => MaybePromise<TStorageValue>;

92

/**

93

* How to deserialize the data from storage.

94

* @default `JSON.parse`

95

*/

96

deserialize?: (cachedString: TStorageValue) => MaybePromise<PersistedQuery>;

97

/**

98

* A unique string that can be used to forcefully invalidate existing caches,

99

* if they do not share the same buster string

100

*/

101

buster?: string;

102

/**

103

* The max-allowed age of the cache in milliseconds.

104

* If a persisted cache is found that is older than this

105

* time, it will be discarded

106

* @default 24 hours

107

*/

108

maxAge?: number;

109

/**

110

* Prefix to be used for storage key.

111

* Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`.

112

* @default 'tanstack-query'

113

*/

114

prefix?: string;

115

/**

116

* Filters to narrow down which Queries should be persisted.

117

*/

118

filters?: QueryFilters;

119

}

120

```

121

122

### AsyncStorage Interface

123

124

Generic interface for storage implementations that can be synchronous or asynchronous.

125

126

```typescript { .api }

127

interface AsyncStorage<TStorageValue = string> {

128

getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>;

129

setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>;

130

removeItem: (key: string) => MaybePromise<void>;

131

/** Optional method for iterating over all stored entries (required for garbage collection and restore) */

132

entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>;

133

}

134

135

type MaybePromise<T> = T | Promise<T>;

136

```

137

138

**Storage Implementation Examples:**

139

140

```typescript

141

// Browser localStorage implementation

142

const localStorageAdapter: AsyncStorage<string> = {

143

getItem: (key) => localStorage.getItem(key),

144

setItem: (key, value) => localStorage.setItem(key, value),

145

removeItem: (key) => localStorage.removeItem(key),

146

entries: () => {

147

const entries: Array<[string, string]> = [];

148

for (let i = 0; i < localStorage.length; i++) {

149

const key = localStorage.key(i);

150

if (key) {

151

const value = localStorage.getItem(key);

152

if (value) entries.push([key, value]);

153

}

154

}

155

return entries;

156

}

157

};

158

159

// React Native AsyncStorage implementation

160

import AsyncStorageRN from '@react-native-async-storage/async-storage';

161

162

const asyncStorageAdapter: AsyncStorage<string> = {

163

getItem: AsyncStorageRN.getItem,

164

setItem: AsyncStorageRN.setItem,

165

removeItem: AsyncStorageRN.removeItem,

166

entries: async () => {

167

const keys = await AsyncStorageRN.getAllKeys();

168

const items = await AsyncStorageRN.multiGet(keys);

169

return items.filter(([, value]) => value !== null) as Array<[string, string]>;

170

}

171

};

172

173

// Custom binary storage implementation

174

const binaryStorageAdapter: AsyncStorage<Uint8Array> = {

175

getItem: async (key) => {

176

const data = await customBinaryStore.get(key);

177

return data || null;

178

},

179

setItem: async (key, value) => {

180

await customBinaryStore.set(key, value);

181

},

182

removeItem: async (key) => {

183

await customBinaryStore.delete(key);

184

},

185

entries: async () => {

186

return await customBinaryStore.getAllEntries();

187

}

188

};

189

```

190

191

### Persisted Query Structure

192

193

Structure used for individual persisted queries.

194

195

```typescript { .api }

196

interface PersistedQuery {

197

buster: string;

198

queryHash: string;

199

queryKey: QueryKey;

200

state: QueryState;

201

}

202

```

203

204

## Advanced Usage

205

206

### Custom Serialization

207

208

```typescript

209

import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core";

210

211

// Custom compression for large data

212

const persister = experimental_createQueryPersister({

213

storage: localStorage,

214

serialize: async (query) => {

215

const json = JSON.stringify(query);

216

return await compress(json); // Custom compression

217

},

218

deserialize: async (compressed) => {

219

const json = await decompress(compressed);

220

return JSON.parse(json);

221

}

222

});

223

```

224

225

### Selective Query Filtering

226

227

```typescript

228

const persister = experimental_createQueryPersister({

229

storage: localStorage,

230

filters: {

231

predicate: (query) => {

232

// Only persist user and settings queries

233

const [queryType] = query.queryKey;

234

return ['user', 'settings'].includes(queryType as string);

235

}

236

}

237

});

238

```

239

240

### Garbage Collection

241

242

```typescript

243

// Clean up expired entries periodically

244

setInterval(async () => {

245

try {

246

await persister.persisterGc();

247

console.log('Cache garbage collection completed');

248

} catch (error) {

249

console.error('Garbage collection failed:', error);

250

}

251

}, 1000 * 60 * 60); // Every hour

252

```

253

254

### Selective Restoration

255

256

```typescript

257

// Restore only specific queries

258

await persister.restoreQueries(queryClient, {

259

queryKey: ['user-data'],

260

exact: false // Partial match - restores 'user-data.*'

261

});

262

263

// Restore exact query

264

await persister.restoreQueries(queryClient, {

265

queryKey: ['user-data', '123'],

266

exact: true

267

});

268

```

269

270

## Constants

271

272

```typescript { .api }

273

/** Default prefix for storage keys */

274

const PERSISTER_KEY_PREFIX = 'tanstack-query';

275

```

276

277

## Error Handling

278

279

Fine-grained persistence includes comprehensive error handling:

280

281

- **Storage Failures**: Gracefully handles storage quota exceeded, permission errors, and network failures

282

- **Serialization Errors**: Catches and logs serialization/deserialization failures

283

- **Expired Data**: Automatically removes expired queries during retrieval

284

- **Development Warnings**: Logs helpful warnings in development mode for debugging

285

- **Storage Feature Detection**: Handles missing storage methods gracefully (e.g., when `entries` is not available)

286

287

All operations are designed to fail gracefully without affecting query execution.