or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

actions-reducers.mdasync-thunks.mdcore-store.mdentity-adapters.mdindex.mdmiddleware.mdreact-integration.mdrtk-query-react.mdrtk-query.mdutilities.md

rtk-query-react.mddocs/

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

```