0
# RTK Query Core
1
2
RTK Query provides a powerful data fetching solution with automatic caching, background updates, and comprehensive state management. Available from `@reduxjs/toolkit/query`.
3
4
## Capabilities
5
6
### Create API
7
8
Creates an RTK Query API slice with endpoints for data fetching and caching.
9
10
```typescript { .api }
11
/**
12
* Creates RTK Query API slice with endpoints for data fetching
13
* @param options - API configuration options
14
* @returns API object with endpoints, reducer, and middleware
15
*/
16
function createApi<
17
BaseQuery extends BaseQueryFn,
18
Definitions extends EndpointDefinitions,
19
ReducerPath extends string = 'api',
20
TagTypes extends string = never
21
>(options: CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>): Api<BaseQuery, Definitions, ReducerPath, TagTypes>;
22
23
interface CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes> {
24
/** Unique key for the API slice in Redux state */
25
reducerPath?: ReducerPath;
26
27
/** Base query function for making requests */
28
baseQuery: BaseQuery;
29
30
/** Cache tag type names for invalidation */
31
tagTypes?: readonly TagTypes[];
32
33
/** Endpoint definitions */
34
endpoints: (builder: EndpointBuilder<BaseQuery, TagTypes, ReducerPath>) => Definitions;
35
36
/** Custom query argument serialization */
37
serializeQueryArgs?: SerializeQueryArgs<any>;
38
39
/** Global cache retention time in seconds */
40
keepUnusedDataFor?: number;
41
42
/** Global refetch behavior on mount or arg change */
43
refetchOnMountOrArgChange?: boolean | number;
44
45
/** Global refetch on window focus */
46
refetchOnFocus?: boolean;
47
48
/** Global refetch on network reconnect */
49
refetchOnReconnect?: boolean;
50
}
51
52
interface Api<BaseQuery, Definitions, ReducerPath, TagTypes> {
53
/** Generated endpoint objects with query/mutation methods */
54
endpoints: ApiEndpoints<Definitions, BaseQuery, TagTypes>;
55
56
/** Reducer function for the API slice */
57
reducer: Reducer<CombinedState<Definitions, TagTypes, ReducerPath>>;
58
59
/** Middleware for handling async operations */
60
middleware: Middleware<{}, any, Dispatch<AnyAction>>;
61
62
/** Utility functions */
63
util: {
64
/** Get running query promises */
65
getRunningOperationPromises(): Record<string, QueryActionCreatorResult<any> | MutationActionCreatorResult<any>>;
66
/** Reset API state */
67
resetApiState(): PayloadAction<void>;
68
/** Invalidate cache tags */
69
invalidateTags(tags: readonly TagDescription<TagTypes>[]): PayloadAction<TagDescription<TagTypes>[]>;
70
/** Select cache entries by tags */
71
selectCachedArgsForQuery<T>(state: any, endpointName: T): readonly unknown[];
72
/** Select invalidated cache entries */
73
selectInvalidatedBy(state: any, tags: readonly TagDescription<TagTypes>[]): readonly unknown[];
74
};
75
76
/** Internal cache key functions */
77
internalActions: {
78
onOnline(): PayloadAction<void>;
79
onOffline(): PayloadAction<void>;
80
onFocus(): PayloadAction<void>;
81
onFocusLost(): PayloadAction<void>;
82
};
83
}
84
```
85
86
**Usage Examples:**
87
88
```typescript
89
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
90
91
// Basic API definition
92
const postsApi = createApi({
93
reducerPath: 'postsApi',
94
baseQuery: fetchBaseQuery({
95
baseUrl: '/api/',
96
}),
97
tagTypes: ['Post', 'User'],
98
endpoints: (builder) => ({
99
getPosts: builder.query<Post[], void>({
100
query: () => 'posts',
101
providesTags: ['Post']
102
}),
103
getPost: builder.query<Post, number>({
104
query: (id) => `posts/${id}`,
105
providesTags: (result, error, id) => [{ type: 'Post', id }]
106
}),
107
addPost: builder.mutation<Post, Partial<Post>>({
108
query: (newPost) => ({
109
url: 'posts',
110
method: 'POST',
111
body: newPost,
112
}),
113
invalidatesTags: ['Post']
114
}),
115
updatePost: builder.mutation<Post, { id: number; patch: Partial<Post> }>({
116
query: ({ id, patch }) => ({
117
url: `posts/${id}`,
118
method: 'PATCH',
119
body: patch,
120
}),
121
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
122
}),
123
deletePost: builder.mutation<{ success: boolean; id: number }, number>({
124
query: (id) => ({
125
url: `posts/${id}`,
126
method: 'DELETE',
127
}),
128
invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
129
})
130
})
131
});
132
133
// Export hooks and actions
134
export const {
135
useGetPostsQuery,
136
useGetPostQuery,
137
useAddPostMutation,
138
useUpdatePostMutation,
139
useDeletePostMutation
140
} = postsApi;
141
142
// Configure store
143
const store = configureStore({
144
reducer: {
145
postsApi: postsApi.reducer,
146
},
147
middleware: (getDefaultMiddleware) =>
148
getDefaultMiddleware().concat(postsApi.middleware)
149
});
150
```
151
152
### Base Query Functions
153
154
RTK Query provides base query implementations for common scenarios.
155
156
```typescript { .api }
157
/**
158
* Built-in base query using fetch API
159
* @param options - Fetch configuration options
160
* @returns Base query function for HTTP requests
161
*/
162
function fetchBaseQuery(options?: FetchBaseQueryArgs): BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>;
163
164
interface FetchBaseQueryArgs {
165
/** Base URL for all requests */
166
baseUrl?: string;
167
168
/** Function to prepare request headers */
169
prepareHeaders?: (headers: Headers, api: { getState: () => unknown; extra: any; endpoint: string; type: 'query' | 'mutation'; forced?: boolean }) => Headers | void;
170
171
/** Custom fetch implementation */
172
fetchFn?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
173
174
/** URL parameter serialization function */
175
paramsSerializer?: (params: Record<string, any>) => string;
176
177
/** Request timeout in milliseconds */
178
timeout?: number;
179
180
/** Response validation function */
181
validateStatus?: (response: Response, body: any) => boolean;
182
}
183
184
interface FetchArgs {
185
/** Request URL */
186
url: string;
187
/** HTTP method */
188
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
189
/** Request headers */
190
headers?: HeadersInit;
191
/** Request body */
192
body?: any;
193
/** URL parameters */
194
params?: Record<string, any>;
195
/** Response parsing method */
196
responseHandler?: 'content-type' | 'json' | 'text' | ((response: Response) => Promise<any>);
197
/** Response validation */
198
validateStatus?: (response: Response, body: any) => boolean;
199
/** Request timeout */
200
timeout?: number;
201
}
202
203
type FetchBaseQueryError =
204
| { status: number; data: any }
205
| { status: 'FETCH_ERROR'; error: string; data?: undefined }
206
| { status: 'PARSING_ERROR'; originalStatus: number; data: string; error: string }
207
| { status: 'TIMEOUT_ERROR'; error: string; data?: undefined }
208
| { status: 'CUSTOM_ERROR'; error: string; data?: any };
209
210
/**
211
* Placeholder base query for APIs that only use queryFn
212
* @returns Fake base query that throws error if used
213
*/
214
function fakeBaseQuery<ErrorType = string>(): BaseQueryFn<any, any, ErrorType>;
215
```
216
217
**Usage Examples:**
218
219
```typescript
220
// Basic fetch base query
221
const api = createApi({
222
baseQuery: fetchBaseQuery({
223
baseUrl: 'https://api.example.com/',
224
prepareHeaders: (headers, { getState }) => {
225
const token = (getState() as RootState).auth.token;
226
if (token) {
227
headers.set('authorization', `Bearer ${token}`);
228
}
229
headers.set('content-type', 'application/json');
230
return headers;
231
},
232
timeout: 10000
233
}),
234
endpoints: (builder) => ({
235
getData: builder.query<Data, string>({
236
query: (id) => ({
237
url: `data/${id}`,
238
params: { include: 'details' }
239
})
240
})
241
})
242
});
243
244
// Custom response handling
245
const apiWithCustomResponse = createApi({
246
baseQuery: fetchBaseQuery({
247
baseUrl: '/api/',
248
validateStatus: (response, result) => {
249
return response.ok && result?.success === true;
250
}
251
}),
252
endpoints: (builder) => ({
253
getProtectedData: builder.query<ProtectedData, void>({
254
query: () => ({
255
url: 'protected',
256
responseHandler: async (response) => {
257
if (!response.ok) {
258
throw new Error(`HTTP ${response.status}`);
259
}
260
const data = await response.json();
261
return data.payload; // Extract nested payload
262
}
263
})
264
})
265
})
266
});
267
268
// Using fakeBaseQuery with queryFn
269
const apiWithCustomLogic = createApi({
270
baseQuery: fakeBaseQuery<CustomError>(),
271
endpoints: (builder) => ({
272
getComplexData: builder.query<ComplexData, string>({
273
queryFn: async (arg, queryApi, extraOptions, baseQuery) => {
274
try {
275
// Custom logic for data fetching
276
const step1 = await customApiClient.fetchUserData(arg);
277
const step2 = await customApiClient.fetchUserPreferences(step1.id);
278
279
return { data: { ...step1, preferences: step2 } };
280
} catch (error) {
281
return { error: { status: 'CUSTOM_ERROR', error: error.message } };
282
}
283
}
284
})
285
})
286
});
287
```
288
289
### Endpoint Definitions
290
291
Define query and mutation endpoints with comprehensive configuration options.
292
293
```typescript { .api }
294
/**
295
* Builder object for defining endpoints
296
*/
297
interface EndpointBuilder<BaseQuery, TagTypes, ReducerPath> {
298
/** Define a query endpoint for data fetching */
299
query<ResultType, QueryArg = void>(
300
definition: QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>
301
): QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>;
302
303
/** Define a mutation endpoint for data modification */
304
mutation<ResultType, QueryArg = void>(
305
definition: MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>
306
): MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath>;
307
308
/** Define an infinite query endpoint for paginated data */
309
infiniteQuery<ResultType, QueryArg = void, PageParam = unknown>(
310
definition: InfiniteQueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, PageParam, ReducerPath>
311
): InfiniteQueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, PageParam, ReducerPath>;
312
}
313
314
interface QueryDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> {
315
/** Query function or args */
316
query: (arg: QueryArg) => any;
317
318
/** Custom query implementation */
319
queryFn?: (arg: QueryArg, api: QueryApi<ReducerPath, any, any, any>, extraOptions: any, baseQuery: BaseQuery) => Promise<QueryFnResult<ResultType>> | QueryFnResult<ResultType>;
320
321
/** Transform the response data */
322
transformResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => ResultType | Promise<ResultType>;
323
324
/** Transform error responses */
325
transformErrorResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => any;
326
327
/** Cache tags this query provides */
328
providesTags?: ResultDescription<TagTypes, ResultType, QueryArg>;
329
330
/** Cache retention time override */
331
keepUnusedDataFor?: number;
332
333
/** Lifecycle callback when query starts */
334
onQueryStarted?: (arg: QueryArg, api: QueryLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
335
336
/** Lifecycle callback when cache entry is added */
337
onCacheEntryAdded?: (arg: QueryArg, api: CacheLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
338
}
339
340
interface MutationDefinition<QueryArg, BaseQuery, TagTypes, ResultType, ReducerPath> {
341
/** Mutation function or args */
342
query: (arg: QueryArg) => any;
343
344
/** Custom mutation implementation */
345
queryFn?: (arg: QueryArg, api: QueryApi<ReducerPath, any, any, any>, extraOptions: any, baseQuery: BaseQuery) => Promise<QueryFnResult<ResultType>> | QueryFnResult<ResultType>;
346
347
/** Transform the response data */
348
transformResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => ResultType | Promise<ResultType>;
349
350
/** Transform error responses */
351
transformErrorResponse?: (baseQueryReturnValue: any, meta: any, arg: QueryArg) => any;
352
353
/** Cache tags to invalidate */
354
invalidatesTags?: ResultDescription<TagTypes, ResultType, QueryArg>;
355
356
/** Lifecycle callback when mutation starts */
357
onQueryStarted?: (arg: QueryArg, api: MutationLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
358
359
/** Lifecycle callback when cache entry is added */
360
onCacheEntryAdded?: (arg: QueryArg, api: CacheLifecycleApi<QueryArg, BaseQuery, ResultType, ReducerPath>) => Promise<void> | void;
361
}
362
363
type ResultDescription<TagTypes, ResultType, QueryArg> =
364
| readonly TagTypes[]
365
| readonly TagDescription<TagTypes>[]
366
| ((result: ResultType | undefined, error: any, arg: QueryArg) => readonly TagDescription<TagTypes>[]);
367
368
interface TagDescription<TagTypes> {
369
type: TagTypes;
370
id?: string | number;
371
}
372
373
interface QueryFnResult<T> {
374
data?: T;
375
error?: any;
376
meta?: any;
377
}
378
```
379
380
**Usage Examples:**
381
382
```typescript
383
const api = createApi({
384
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
385
tagTypes: ['Post', 'User', 'Comment'],
386
endpoints: (builder) => ({
387
// Simple query
388
getPosts: builder.query<Post[], void>({
389
query: () => 'posts',
390
providesTags: ['Post']
391
}),
392
393
// Query with parameters
394
getPostsByUser: builder.query<Post[], { userId: string; limit?: number }>({
395
query: ({ userId, limit = 10 }) => ({
396
url: 'posts',
397
params: { userId, limit }
398
}),
399
providesTags: (result, error, { userId }) => [
400
'Post',
401
{ type: 'User', id: userId }
402
]
403
}),
404
405
// Query with response transformation
406
getPostWithComments: builder.query<PostWithComments, string>({
407
query: (id) => `posts/${id}`,
408
transformResponse: (response: { post: Post; comments: Comment[] }) => ({
409
...response.post,
410
comments: response.comments.sort((a, b) =>
411
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
412
)
413
}),
414
providesTags: (result, error, id) => [
415
{ type: 'Post', id },
416
{ type: 'Comment', id: 'LIST' }
417
]
418
}),
419
420
// Custom query function
421
getAnalytics: builder.query<Analytics, { startDate: string; endDate: string }>({
422
queryFn: async (arg, queryApi, extraOptions, baseQuery) => {
423
try {
424
// Multiple API calls
425
const [posts, users, comments] = await Promise.all([
426
baseQuery(`analytics/posts?start=${arg.startDate}&end=${arg.endDate}`),
427
baseQuery(`analytics/users?start=${arg.startDate}&end=${arg.endDate}`),
428
baseQuery(`analytics/comments?start=${arg.startDate}&end=${arg.endDate}`)
429
]);
430
431
if (posts.error || users.error || comments.error) {
432
return { error: 'Failed to fetch analytics data' };
433
}
434
435
return {
436
data: {
437
posts: posts.data,
438
users: users.data,
439
comments: comments.data,
440
summary: calculateSummary(posts.data, users.data, comments.data)
441
}
442
};
443
} catch (error) {
444
return { error: error.message };
445
}
446
}
447
}),
448
449
// Mutation with optimistic updates
450
updatePost: builder.mutation<Post, { id: string; patch: Partial<Post> }>({
451
query: ({ id, patch }) => ({
452
url: `posts/${id}`,
453
method: 'PATCH',
454
body: patch
455
}),
456
onQueryStarted: async ({ id, patch }, { dispatch, queryFulfilled }) => {
457
// Optimistic update
458
const patchResult = dispatch(
459
api.util.updateQueryData('getPost', id, (draft) => {
460
Object.assign(draft, patch);
461
})
462
);
463
464
try {
465
await queryFulfilled;
466
} catch {
467
// Revert on failure
468
patchResult.undo();
469
}
470
},
471
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
472
}),
473
474
// Mutation with side effects
475
deletePost: builder.mutation<void, string>({
476
query: (id) => ({
477
url: `posts/${id}`,
478
method: 'DELETE'
479
}),
480
onQueryStarted: async (id, { dispatch, queryFulfilled }) => {
481
try {
482
await queryFulfilled;
483
484
// Additional side effects
485
dispatch(showNotification('Post deleted successfully'));
486
dispatch(api.util.invalidateTags([{ type: 'Post', id }]));
487
} catch (error) {
488
dispatch(showNotification('Failed to delete post'));
489
}
490
}
491
})
492
})
493
});
494
```
495
496
### Cache Management
497
498
RTK Query provides sophisticated caching with tag-based invalidation and cache lifecycle management.
499
500
```typescript { .api }
501
/**
502
* Skip query execution when passed as argument
503
*/
504
const skipToken: unique symbol;
505
506
/**
507
* Query status enumeration
508
*/
509
enum QueryStatus {
510
uninitialized = 'uninitialized',
511
pending = 'pending',
512
fulfilled = 'fulfilled',
513
rejected = 'rejected'
514
}
515
516
/**
517
* Cache state structure for queries and mutations
518
*/
519
interface QueryState<T> {
520
/** Query status */
521
status: QueryStatus;
522
/** Query result data */
523
data?: T;
524
/** Current request ID */
525
requestId?: string;
526
/** Error information */
527
error?: any;
528
/** Fulfillment timestamp */
529
fulfilledTimeStamp?: number;
530
/** Start timestamp */
531
startedTimeStamp?: number;
532
/** End timestamp */
533
endedTimeStamp?: number;
534
}
535
536
interface MutationState<T> {
537
/** Mutation status */
538
status: QueryStatus;
539
/** Mutation result data */
540
data?: T;
541
/** Current request ID */
542
requestId?: string;
543
/** Error information */
544
error?: any;
545
}
546
```
547
548
**Usage Examples:**
549
550
```typescript
551
// Using skipToken to conditionally skip queries
552
const UserProfile = ({ userId }: { userId?: string }) => {
553
const { data: user, error, isLoading } = useGetUserQuery(
554
userId ?? skipToken // Skip query if no userId
555
);
556
557
if (!userId) {
558
return <div>Please select a user</div>;
559
}
560
561
if (isLoading) return <div>Loading...</div>;
562
if (error) return <div>Error loading user</div>;
563
564
return <div>{user?.name}</div>;
565
};
566
567
// Manual cache updates
568
const PostsList = () => {
569
const { data: posts } = useGetPostsQuery();
570
const [updatePost] = useUpdatePostMutation();
571
572
const handleOptimisticUpdate = (id: string, changes: Partial<Post>) => {
573
// Manually update cache
574
store.dispatch(
575
api.util.updateQueryData('getPosts', undefined, (draft) => {
576
const post = draft.find(p => p.id === id);
577
if (post) {
578
Object.assign(post, changes);
579
}
580
})
581
);
582
583
// Then perform actual update
584
updatePost({ id, patch: changes });
585
};
586
587
return (
588
<div>
589
{posts?.map(post => (
590
<PostItem
591
key={post.id}
592
post={post}
593
onUpdate={handleOptimisticUpdate}
594
/>
595
))}
596
</div>
597
);
598
};
599
600
// Prefetching data
601
const PostsListWithPrefetch = () => {
602
const dispatch = useAppDispatch();
603
604
useEffect(() => {
605
// Prefetch data
606
dispatch(api.util.prefetch('getPosts', undefined, { force: false }));
607
608
// Prefetch related data
609
dispatch(api.util.prefetch('getUsers', undefined));
610
}, [dispatch]);
611
612
const { data: posts } = useGetPostsQuery();
613
614
return <div>...</div>;
615
};
616
```
617
618
## Advanced Features
619
620
### Streaming Updates
621
622
```typescript
623
// Real-time updates with cache entry lifecycle
624
const api = createApi({
625
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
626
endpoints: (builder) => ({
627
getPosts: builder.query<Post[], void>({
628
query: () => 'posts',
629
onCacheEntryAdded: async (
630
arg,
631
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
632
) => {
633
// Wait for initial data load
634
await cacheDataLoaded;
635
636
// Setup WebSocket connection
637
const ws = new WebSocket('ws://localhost:8080/posts');
638
639
ws.addEventListener('message', (event) => {
640
const update = JSON.parse(event.data);
641
642
updateCachedData((draft) => {
643
switch (update.type) {
644
case 'POST_ADDED':
645
draft.push(update.post);
646
break;
647
case 'POST_UPDATED':
648
const index = draft.findIndex(p => p.id === update.post.id);
649
if (index !== -1) {
650
draft[index] = update.post;
651
}
652
break;
653
case 'POST_DELETED':
654
return draft.filter(p => p.id !== update.postId);
655
}
656
});
657
});
658
659
// Cleanup on cache entry removal
660
await cacheEntryRemoved;
661
ws.close();
662
}
663
})
664
})
665
});
666
```
667
668
### Request Deduplication and Polling
669
670
```typescript
671
const api = createApi({
672
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
673
endpoints: (builder) => ({
674
// Polling query
675
getLiveData: builder.query<LiveData, void>({
676
query: () => 'live-data',
677
// Automatic polling every 5 seconds
678
pollingInterval: 5000
679
}),
680
681
// Custom request deduplication
682
getExpensiveData: builder.query<ExpensiveData, string>({
683
query: (id) => `expensive-data/${id}`,
684
serializeQueryArgs: ({ queryArgs, endpointDefinition, endpointName }) => {
685
// Custom serialization for deduplication
686
return `${endpointName}(${queryArgs})`;
687
},
688
// Cache for 10 minutes
689
keepUnusedDataFor: 600
690
})
691
})
692
});
693
694
// Component with polling control
695
const LiveDataComponent = () => {
696
const [pollingEnabled, setPollingEnabled] = useState(true);
697
698
const { data, error, isLoading } = useGetLiveDataQuery(undefined, {
699
pollingInterval: pollingEnabled ? 5000 : 0,
700
skipPollingIfUnfocused: true
701
});
702
703
return (
704
<div>
705
<button onClick={() => setPollingEnabled(!pollingEnabled)}>
706
{pollingEnabled ? 'Stop' : 'Start'} Polling
707
</button>
708
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
709
</div>
710
);
711
};
712
```
713
714
### Background Refetching
715
716
```typescript
717
// Setup automatic refetching listeners
718
import { setupListeners } from '@reduxjs/toolkit/query';
719
720
// Enable refetching on focus/reconnect
721
setupListeners(store.dispatch, {
722
onFocus: () => console.log('Window focused'),
723
onFocusLost: () => console.log('Window focus lost'),
724
onOnline: () => console.log('Network online'),
725
onOffline: () => console.log('Network offline')
726
});
727
728
// API with custom refetch behavior
729
const api = createApi({
730
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
731
refetchOnFocus: true,
732
refetchOnReconnect: true,
733
endpoints: (builder) => ({
734
getCriticalData: builder.query<CriticalData, void>({
735
query: () => 'critical-data',
736
// Always refetch on mount
737
refetchOnMountOrArgChange: true
738
}),
739
740
getCachedData: builder.query<CachedData, void>({
741
query: () => 'cached-data',
742
// Only refetch if older than 5 minutes
743
refetchOnMountOrArgChange: 300
744
})
745
})
746
});
747
```
748
749
### RTK Query Utilities
750
751
Additional utility functions for advanced RTK Query usage.
752
753
```typescript { .api }
754
/**
755
* Default function for serializing query arguments into cache keys
756
* @param args - Query arguments to serialize
757
* @param endpointDefinition - Endpoint definition for context
758
* @param endpointName - Name of the endpoint
759
* @returns Serialized string representation of arguments
760
*/
761
function defaultSerializeQueryArgs(
762
args: any,
763
endpointDefinition: EndpointDefinition<any, any, any, any>,
764
endpointName: string
765
): string;
766
767
/**
768
* Copy value while preserving structural sharing for performance
769
* Used internally for immutable updates with minimal re-renders
770
* @param oldObj - Previous value
771
* @param newObj - New value to merge
772
* @returns Value with structural sharing where possible
773
*/
774
function copyWithStructuralSharing<T>(oldObj: T, newObj: T): T;
775
776
/**
777
* Retry utility for enhanced error handling in base queries
778
* @param baseQuery - Base query function to retry
779
* @param options - Retry configuration options
780
* @returns Enhanced base query with retry logic
781
*/
782
function retry<Args extends any, Return extends any, Error extends any>(
783
baseQuery: BaseQueryFn<Args, Return, Error>,
784
options: RetryOptions
785
): BaseQueryFn<Args, Return, Error>;
786
787
interface RetryOptions {
788
/** Maximum number of retry attempts */
789
maxRetries: number;
790
/** Function to determine if error should trigger retry */
791
retryCondition?: (error: any, args: any, extraOptions: any) => boolean;
792
/** Delay between retries in milliseconds */
793
backoff?: (attempt: number, maxRetries: number) => number;
794
}
795
```
796
797
**Usage Examples:**
798
799
```typescript
800
import {
801
defaultSerializeQueryArgs,
802
copyWithStructuralSharing,
803
retry
804
} from '@reduxjs/toolkit/query';
805
806
// Custom query argument serialization
807
const customApi = createApi({
808
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
809
serializeQueryArgs: ({ queryArgs, endpointDefinition, endpointName }) => {
810
// Use default serialization with custom prefix
811
const defaultKey = defaultSerializeQueryArgs(queryArgs, endpointDefinition, endpointName);
812
return `custom:${defaultKey}`;
813
},
814
endpoints: (builder) => ({
815
getUser: builder.query<User, { userId: string; include?: string[] }>({
816
query: ({ userId, include = [] }) => ({
817
url: `users/${userId}`,
818
params: { include: include.join(',') }
819
})
820
})
821
})
822
});
823
824
// Enhanced base query with retry logic
825
const resilientBaseQuery = retry(
826
fetchBaseQuery({ baseUrl: '/api' }),
827
{
828
maxRetries: 3,
829
retryCondition: (error, args, extraOptions) => {
830
// Retry on network errors and 5xx server errors
831
return error.status >= 500 || error.name === 'NetworkError';
832
},
833
backoff: (attempt, maxRetries) => {
834
// Exponential backoff: 1s, 2s, 4s
835
return Math.min(1000 * (2 ** attempt), 10000);
836
}
837
}
838
);
839
840
const apiWithRetry = createApi({
841
baseQuery: resilientBaseQuery,
842
endpoints: (builder) => ({
843
getCriticalData: builder.query<Data, void>({
844
query: () => 'critical-data'
845
})
846
})
847
});
848
849
// Structural sharing in transformResponse
850
const optimizedApi = createApi({
851
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
852
endpoints: (builder) => ({
853
getOptimizedData: builder.query<ProcessedData, void>({
854
query: () => 'data',
855
transformResponse: (response: RawData, meta, arg) => {
856
const processed = processData(response);
857
// copyWithStructuralSharing is used internally by RTK Query
858
// but can be useful for custom transform logic
859
return {
860
...processed,
861
metadata: {
862
fetchedAt: Date.now(),
863
version: response.version
864
}
865
};
866
}
867
})
868
})
869
});
870
```