0
# RTK Query React
1
2
RTK Query React provides React-specific hooks and components for seamless integration with RTK Query data fetching. Available from `@reduxjs/toolkit/query/react`.
3
4
## Capabilities
5
6
### Auto-generated Hooks
7
8
RTK Query automatically generates React hooks for each endpoint, providing type-safe data fetching and state management.
9
10
```typescript { .api }
11
/**
12
* Auto-generated query hook for data fetching
13
* Generated for each query endpoint as `use{EndpointName}Query`
14
*/
15
type UseQueryHook<ResultType, QueryArg> = (
16
arg: QueryArg,
17
options?: UseQueryOptions<QueryArg>
18
) => UseQueryResult<ResultType>;
19
20
interface UseQueryOptions<QueryArg> {
21
/** Skip query execution */
22
skip?: boolean;
23
/** Polling interval in milliseconds */
24
pollingInterval?: number;
25
/** Skip polling when window is unfocused */
26
skipPollingIfUnfocused?: boolean;
27
/** Refetch on mount or arg change */
28
refetchOnMountOrArgChange?: boolean | number;
29
/** Refetch on window focus */
30
refetchOnFocus?: boolean;
31
/** Refetch on network reconnect */
32
refetchOnReconnect?: boolean;
33
/** Transform the hook result */
34
selectFromResult?: (result: UseQueryStateDefaultResult<ResultType>) => any;
35
}
36
37
interface UseQueryResult<ResultType> {
38
/** Query result data */
39
data: ResultType | undefined;
40
/** Current query error */
41
error: any;
42
/** True during initial load */
43
isLoading: boolean;
44
/** True during any fetch operation */
45
isFetching: boolean;
46
/** True when query succeeded */
47
isSuccess: boolean;
48
/** True when query failed */
49
isError: boolean;
50
/** True when query has never been run */
51
isUninitialized: boolean;
52
/** Current query status */
53
status: QueryStatus;
54
/** Current request ID */
55
requestId: string;
56
/** Request start timestamp */
57
startedTimeStamp?: number;
58
/** Request fulfillment timestamp */
59
fulfilledTimeStamp?: number;
60
/** Current query arguments */
61
originalArgs?: any;
62
/** Manual refetch function */
63
refetch: () => QueryActionCreatorResult<any>;
64
}
65
66
/**
67
* Auto-generated lazy query hook for manual triggering
68
* Generated for each query endpoint as `useLazy{EndpointName}Query`
69
*/
70
type UseLazyQueryHook<ResultType, QueryArg> = (
71
options?: UseLazyQueryOptions
72
) => [
73
(arg: QueryArg, preferCacheValue?: boolean) => QueryActionCreatorResult<ResultType>,
74
UseQueryResult<ResultType>
75
];
76
77
interface UseLazyQueryOptions {
78
/** Transform the hook result */
79
selectFromResult?: (result: UseQueryStateDefaultResult<any>) => any;
80
}
81
82
/**
83
* Auto-generated mutation hook for data modification
84
* Generated for each mutation endpoint as `use{EndpointName}Mutation`
85
*/
86
type UseMutationHook<ResultType, QueryArg> = (
87
options?: UseMutationOptions
88
) => [
89
(arg: QueryArg) => MutationActionCreatorResult<ResultType>,
90
UseMutationResult<ResultType>
91
];
92
93
interface UseMutationOptions {
94
/** Transform the hook result */
95
selectFromResult?: (result: UseMutationStateDefaultResult<any>) => any;
96
}
97
98
interface UseMutationResult<ResultType> {
99
/** Mutation result data */
100
data: ResultType | undefined;
101
/** Mutation error */
102
error: any;
103
/** True during mutation */
104
isLoading: boolean;
105
/** True when mutation succeeded */
106
isSuccess: boolean;
107
/** True when mutation failed */
108
isError: boolean;
109
/** True when mutation has never been called */
110
isUninitialized: boolean;
111
/** Current mutation status */
112
status: QueryStatus;
113
/** Reset mutation state */
114
reset: () => void;
115
/** Original arguments passed to mutation */
116
originalArgs?: any;
117
/** Request start timestamp */
118
startedTimeStamp?: number;
119
/** Request fulfillment timestamp */
120
fulfilledTimeStamp?: number;
121
}
122
```
123
124
**Usage Examples:**
125
126
```typescript
127
import { api } from './api';
128
129
// Using auto-generated query hooks
130
const PostsList = () => {
131
const {
132
data: posts,
133
error,
134
isLoading,
135
isFetching,
136
refetch
137
} = useGetPostsQuery();
138
139
const {
140
data: user,
141
isLoading: userLoading
142
} = useGetCurrentUserQuery(undefined, {
143
skip: !posts, // Skip until posts are loaded
144
pollingInterval: 60000 // Poll every minute
145
});
146
147
if (isLoading) return <div>Loading posts...</div>;
148
if (error) return <div>Error: {error.message}</div>;
149
150
return (
151
<div>
152
<button onClick={refetch} disabled={isFetching}>
153
{isFetching ? 'Refreshing...' : 'Refresh'}
154
</button>
155
{posts?.map(post => (
156
<PostItem key={post.id} post={post} />
157
))}
158
</div>
159
);
160
};
161
162
// Using lazy query hook
163
const SearchComponent = () => {
164
const [searchTerm, setSearchTerm] = useState('');
165
const [triggerSearch, { data: results, isLoading, error }] = useLazyGetSearchResultsQuery();
166
167
const handleSearch = () => {
168
if (searchTerm.trim()) {
169
triggerSearch(searchTerm);
170
}
171
};
172
173
return (
174
<div>
175
<input
176
value={searchTerm}
177
onChange={(e) => setSearchTerm(e.target.value)}
178
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
179
/>
180
<button onClick={handleSearch} disabled={isLoading}>
181
{isLoading ? 'Searching...' : 'Search'}
182
</button>
183
{results && (
184
<div>
185
{results.map(item => <SearchResult key={item.id} item={item} />)}
186
</div>
187
)}
188
</div>
189
);
190
};
191
192
// Using mutation hook
193
const AddPostForm = () => {
194
const [title, setTitle] = useState('');
195
const [content, setContent] = useState('');
196
const [addPost, { isLoading, error, isSuccess }] = useAddPostMutation();
197
198
const handleSubmit = async (e: React.FormEvent) => {
199
e.preventDefault();
200
try {
201
await addPost({ title, content }).unwrap();
202
setTitle('');
203
setContent('');
204
} catch (error) {
205
console.error('Failed to add post:', error);
206
}
207
};
208
209
useEffect(() => {
210
if (isSuccess) {
211
alert('Post added successfully!');
212
}
213
}, [isSuccess]);
214
215
return (
216
<form onSubmit={handleSubmit}>
217
<input
218
value={title}
219
onChange={(e) => setTitle(e.target.value)}
220
placeholder="Post title"
221
required
222
/>
223
<textarea
224
value={content}
225
onChange={(e) => setContent(e.target.value)}
226
placeholder="Post content"
227
required
228
/>
229
<button type="submit" disabled={isLoading}>
230
{isLoading ? 'Adding...' : 'Add Post'}
231
</button>
232
{error && <div>Error: {error.message}</div>}
233
</form>
234
);
235
};
236
```
237
238
### Query State Management Hooks
239
240
Additional hooks for fine-grained control over query state without triggering new requests.
241
242
```typescript { .api }
243
/**
244
* Hook to access query state without subscription
245
* Generated for each query endpoint as `use{EndpointName}QueryState`
246
*/
247
type UseQueryStateHook<ResultType, QueryArg> = (
248
arg: QueryArg,
249
options?: UseQueryStateOptions
250
) => UseQueryResult<ResultType>;
251
252
interface UseQueryStateOptions {
253
/** Skip the hook */
254
skip?: boolean;
255
/** Transform the hook result */
256
selectFromResult?: (result: UseQueryStateDefaultResult<any>) => any;
257
}
258
259
/**
260
* Hook for query subscription without data access
261
* Generated for each query endpoint as `use{EndpointName}QuerySubscription`
262
*/
263
type UseQuerySubscriptionHook<QueryArg> = (
264
arg: QueryArg,
265
options?: UseQuerySubscriptionOptions<QueryArg>
266
) => Pick<UseQueryResult<any>, 'refetch'>;
267
268
interface UseQuerySubscriptionOptions<QueryArg> extends UseQueryOptions<QueryArg> {
269
/** Skip the subscription */
270
skip?: boolean;
271
}
272
273
/**
274
* Lazy version of query subscription hook
275
* Generated for each query endpoint as `useLazy{EndpointName}QuerySubscription`
276
*/
277
type UseLazyQuerySubscriptionHook<QueryArg> = () => [
278
(arg: QueryArg) => QueryActionCreatorResult<any>,
279
Pick<UseQueryResult<any>, 'refetch'>
280
];
281
```
282
283
**Usage Examples:**
284
285
```typescript
286
// Separate data access and subscription
287
const PostsListOptimized = () => {
288
// Subscribe to updates but don't access data here
289
const { refetch } = useGetPostsQuerySubscription();
290
291
return (
292
<div>
293
<PostsListData />
294
<button onClick={refetch}>Refresh</button>
295
</div>
296
);
297
};
298
299
const PostsListData = () => {
300
// Access data without creating new subscription
301
const { data: posts, isLoading } = useGetPostsQueryState();
302
303
if (isLoading) return <div>Loading...</div>;
304
305
return (
306
<div>
307
{posts?.map(post => <PostItem key={post.id} post={post} />)}
308
</div>
309
);
310
};
311
312
// Conditional subscriptions
313
const ConditionalDataComponent = ({ userId }: { userId?: string }) => {
314
// Only subscribe when we have a user ID
315
useGetUserDataQuerySubscription(userId!, {
316
skip: !userId,
317
pollingInterval: 30000
318
});
319
320
// Access the data separately
321
const { data: userData } = useGetUserDataQueryState(userId!, {
322
skip: !userId
323
});
324
325
return userId ? <UserProfile user={userData} /> : <div>No user selected</div>;
326
};
327
```
328
329
### Result Selection and Transformation
330
331
Transform and select specific parts of query results for optimized re-renders.
332
333
```typescript { .api }
334
/**
335
* Transform query result before returning from hook
336
*/
337
interface SelectFromResultOptions<T> {
338
selectFromResult?: (result: UseQueryStateDefaultResult<T>) => any;
339
}
340
341
interface UseQueryStateDefaultResult<T> {
342
data: T | undefined;
343
error: any;
344
isLoading: boolean;
345
isFetching: boolean;
346
isSuccess: boolean;
347
isError: boolean;
348
isUninitialized: boolean;
349
status: QueryStatus;
350
requestId: string;
351
startedTimeStamp?: number;
352
fulfilledTimeStamp?: number;
353
originalArgs?: any;
354
refetch: () => QueryActionCreatorResult<any>;
355
}
356
```
357
358
**Usage Examples:**
359
360
```typescript
361
// Select only specific data to minimize re-renders
362
const UserName = ({ userId }: { userId: string }) => {
363
const userName = useGetUserQuery(userId, {
364
selectFromResult: ({ data, isLoading, error }) => ({
365
name: data?.name,
366
isLoading,
367
error
368
})
369
});
370
371
// Component only re-renders when name, loading state, or error changes
372
if (userName.isLoading) return <div>Loading...</div>;
373
if (userName.error) return <div>Error loading user</div>;
374
375
return <div>{userName.name}</div>;
376
};
377
378
// Select derived data
379
const PostsStats = () => {
380
const stats = useGetPostsQuery(undefined, {
381
selectFromResult: ({ data, isLoading }) => ({
382
totalPosts: data?.length ?? 0,
383
publishedPosts: data?.filter(p => p.published).length ?? 0,
384
isLoading
385
})
386
});
387
388
return (
389
<div>
390
<p>Total: {stats.totalPosts}</p>
391
<p>Published: {stats.publishedPosts}</p>
392
</div>
393
);
394
};
395
396
// Combine multiple query results
397
const DashboardSummary = () => {
398
const summary = useCombinedQueries();
399
400
return <div>{JSON.stringify(summary)}</div>;
401
};
402
403
const useCombinedQueries = () => {
404
const postsResult = useGetPostsQuery();
405
const usersResult = useGetUsersQuery();
406
const commentsResult = useGetCommentsQuery();
407
408
return useMemo(() => {
409
if (postsResult.isLoading || usersResult.isLoading || commentsResult.isLoading) {
410
return { isLoading: true };
411
}
412
413
if (postsResult.error || usersResult.error || commentsResult.error) {
414
return {
415
error: postsResult.error || usersResult.error || commentsResult.error
416
};
417
}
418
419
return {
420
isLoading: false,
421
data: {
422
totalPosts: postsResult.data?.length ?? 0,
423
totalUsers: usersResult.data?.length ?? 0,
424
totalComments: commentsResult.data?.length ?? 0,
425
latestPost: postsResult.data?.[0],
426
activeUsers: usersResult.data?.filter(u => u.isActive).length ?? 0
427
}
428
};
429
}, [postsResult, usersResult, commentsResult]);
430
};
431
```
432
433
### API Provider Component
434
435
Standalone provider for using RTK Query without Redux store setup.
436
437
```typescript { .api }
438
/**
439
* Provider component for standalone RTK Query usage
440
* @param props - Provider configuration
441
* @returns JSX element wrapping children with RTK Query context
442
*/
443
function ApiProvider<A extends Api<any, {}, any, any>>(props: {
444
/** RTK Query API instance */
445
api: A;
446
/** Enable automatic listeners setup */
447
setupListeners?: boolean | ((dispatch: ThunkDispatch<any, any, any>) => () => void);
448
/** React children */
449
children: React.ReactNode;
450
}): JSX.Element;
451
```
452
453
**Usage Examples:**
454
455
```typescript
456
import { ApiProvider } from '@reduxjs/toolkit/query/react';
457
import { api } from './api';
458
459
// Standalone RTK Query usage without Redux store
460
const App = () => {
461
return (
462
<ApiProvider
463
api={api}
464
setupListeners={true} // Enable automatic refetch on focus/reconnect
465
>
466
<PostsList />
467
<AddPostForm />
468
</ApiProvider>
469
);
470
};
471
472
// Custom listener setup
473
const AppWithCustomListeners = () => {
474
const setupCustomListeners = useCallback((dispatch) => {
475
// Custom listener setup
476
const unsubscribe = setupListeners(dispatch, {
477
onFocus: () => console.log('App focused'),
478
onOnline: () => console.log('App online')
479
});
480
481
return unsubscribe;
482
}, []);
483
484
return (
485
<ApiProvider
486
api={api}
487
setupListeners={setupCustomListeners}
488
>
489
<AppContent />
490
</ApiProvider>
491
);
492
};
493
494
// Multiple API providers
495
const MultiApiApp = () => {
496
return (
497
<ApiProvider api={postsApi}>
498
<ApiProvider api={usersApi}>
499
<AppContent />
500
</ApiProvider>
501
</ApiProvider>
502
);
503
};
504
```
505
506
### Infinite Query Hooks
507
508
Special hooks for handling paginated/infinite data patterns.
509
510
```typescript { .api }
511
/**
512
* Auto-generated infinite query hook
513
* Generated for each infinite query endpoint as `use{EndpointName}InfiniteQuery`
514
*/
515
type UseInfiniteQueryHook<ResultType, QueryArg, PageParam> = (
516
arg: QueryArg,
517
options?: UseInfiniteQueryOptions<QueryArg>
518
) => UseInfiniteQueryResult<ResultType, PageParam>;
519
520
interface UseInfiniteQueryOptions<QueryArg> extends UseQueryOptions<QueryArg> {
521
/** Maximum number of pages to keep in cache */
522
maxPages?: number;
523
}
524
525
interface UseInfiniteQueryResult<ResultType, PageParam> {
526
/** All pages of data */
527
data: ResultType | undefined;
528
/** Current query error */
529
error: any;
530
/** Loading states */
531
isLoading: boolean;
532
isFetching: boolean;
533
isFetchingNextPage: boolean;
534
isFetchingPreviousPage: boolean;
535
/** Success/error states */
536
isSuccess: boolean;
537
isError: boolean;
538
/** Pagination info */
539
hasNextPage: boolean;
540
hasPreviousPage: boolean;
541
/** Pagination actions */
542
fetchNextPage: () => void;
543
fetchPreviousPage: () => void;
544
/** Refetch all pages */
545
refetch: () => void;
546
}
547
```
548
549
**Usage Examples:**
550
551
```typescript
552
// Define infinite query endpoint
553
const api = createApi({
554
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
555
endpoints: (builder) => ({
556
getPostsInfinite: builder.infiniteQuery<
557
{ posts: Post[]; total: number; page: number },
558
{ limit: number },
559
number
560
>({
561
query: ({ limit = 10 }) => ({
562
url: 'posts',
563
params: { limit, offset: 0 }
564
}),
565
getNextPageParam: (lastPage, allPages) => {
566
const totalLoaded = allPages.length * 10;
567
return totalLoaded < lastPage.total ? allPages.length + 1 : undefined;
568
},
569
getCombinedResult: (pages) => ({
570
posts: pages.flatMap(page => page.posts),
571
total: pages[0]?.total ?? 0,
572
currentPage: pages.length
573
})
574
})
575
})
576
});
577
578
// Using infinite query hook
579
const InfinitePostsList = () => {
580
const {
581
data,
582
error,
583
isLoading,
584
isFetchingNextPage,
585
hasNextPage,
586
fetchNextPage
587
} = useGetPostsInfiniteQuery({ limit: 10 });
588
589
if (isLoading) return <div>Loading...</div>;
590
if (error) return <div>Error: {error.message}</div>;
591
592
return (
593
<div>
594
{data?.posts.map(post => (
595
<PostItem key={post.id} post={post} />
596
))}
597
598
{hasNextPage && (
599
<button
600
onClick={fetchNextPage}
601
disabled={isFetchingNextPage}
602
>
603
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
604
</button>
605
)}
606
</div>
607
);
608
};
609
610
// Infinite scroll implementation
611
const InfiniteScrollPosts = () => {
612
const {
613
data,
614
fetchNextPage,
615
hasNextPage,
616
isFetchingNextPage
617
} = useGetPostsInfiniteQuery({ limit: 20 });
618
619
const loadMoreRef = useRef<HTMLDivElement>(null);
620
621
useEffect(() => {
622
const observer = new IntersectionObserver(
623
(entries) => {
624
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
625
fetchNextPage();
626
}
627
},
628
{ threshold: 1.0 }
629
);
630
631
if (loadMoreRef.current) {
632
observer.observe(loadMoreRef.current);
633
}
634
635
return () => observer.disconnect();
636
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
637
638
return (
639
<div>
640
{data?.posts.map(post => (
641
<PostItem key={post.id} post={post} />
642
))}
643
<div ref={loadMoreRef}>
644
{isFetchingNextPage && <div>Loading more posts...</div>}
645
</div>
646
</div>
647
);
648
};
649
```
650
651
## Advanced Patterns
652
653
### Optimistic Updates with Error Recovery
654
655
```typescript
656
const OptimisticPostEditor = ({ postId }: { postId: string }) => {
657
const [updatePost] = useUpdatePostMutation();
658
const queryClient = useQueryClient();
659
660
const handleOptimisticUpdate = async (changes: Partial<Post>) => {
661
// Store original data for potential rollback
662
const previousPost = queryClient.getQueryData(['post', postId]);
663
664
// Optimistically update UI
665
queryClient.setQueryData(['post', postId], (old: Post | undefined) =>
666
old ? { ...old, ...changes } : undefined
667
);
668
669
try {
670
await updatePost({ id: postId, patch: changes }).unwrap();
671
} catch (error) {
672
// Rollback on error
673
queryClient.setQueryData(['post', postId], previousPost);
674
throw error;
675
}
676
};
677
678
return <PostEditor onSave={handleOptimisticUpdate} />;
679
};
680
```
681
682
### Synchronized Queries
683
684
```typescript
685
// Keep multiple related queries in sync
686
const SynchronizedDataComponent = ({ userId }: { userId: string }) => {
687
const { data: user } = useGetUserQuery(userId);
688
const { data: posts } = useGetUserPostsQuery(userId, {
689
skip: !user // Wait for user data
690
});
691
const { data: profile } = useGetUserProfileQuery(userId, {
692
skip: !user
693
});
694
695
// All queries are synchronized - profile and posts only load after user
696
return (
697
<div>
698
{user && <UserInfo user={user} />}
699
{posts && <PostsList posts={posts} />}
700
{profile && <UserProfile profile={profile} />}
701
</div>
702
);
703
};
704
```
705
706
### Custom Hook Composition
707
708
```typescript
709
// Compose multiple RTK Query hooks into custom hooks
710
const usePostWithAuthor = (postId: string) => {
711
const { data: post, ...postQuery } = useGetPostQuery(postId);
712
const { data: author, ...authorQuery } = useGetUserQuery(post?.authorId!, {
713
skip: !post?.authorId
714
});
715
716
return {
717
post,
718
author,
719
isLoading: postQuery.isLoading || authorQuery.isLoading,
720
error: postQuery.error || authorQuery.error,
721
refetch: () => {
722
postQuery.refetch();
723
if (post?.authorId) {
724
authorQuery.refetch();
725
}
726
}
727
};
728
};
729
730
// Usage
731
const PostWithAuthorComponent = ({ postId }: { postId: string }) => {
732
const { post, author, isLoading, error } = usePostWithAuthor(postId);
733
734
if (isLoading) return <div>Loading...</div>;
735
if (error) return <div>Error loading post</div>;
736
737
return (
738
<article>
739
<h1>{post?.title}</h1>
740
<p>By {author?.name}</p>
741
<div>{post?.content}</div>
742
</article>
743
);
744
};
745
```
746
747
### RTK Query React Constants
748
749
Constants and utilities specific to RTK Query React integration.
750
751
```typescript { .api }
752
/**
753
* Special value representing uninitialized query state
754
* Used internally by RTK Query React hooks
755
*/
756
const UNINITIALIZED_VALUE: unique symbol;
757
```
758
759
**Usage Examples:**
760
761
```typescript
762
import { UNINITIALIZED_VALUE } from '@reduxjs/toolkit/query/react';
763
764
// Check if query result is truly uninitialized (vs undefined data)
765
const MyComponent = () => {
766
const { data, isUninitialized } = useGetDataQuery();
767
768
// Direct comparison (rarely needed in application code)
769
if (data === UNINITIALIZED_VALUE) {
770
console.log('Query has not been started');
771
}
772
773
// Prefer using the isUninitialized flag
774
if (isUninitialized) {
775
return <div>Query not started</div>;
776
}
777
778
return <div>{data ? 'Has data' : 'No data'}</div>;
779
};
780
```