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.