or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdcore-query-hooks.mdindex.mdmutations.mdproviders-context.mdsuspense-integration.mdutilities.md

configuration.mddocs/

0

# Configuration Helpers

1

2

This document covers type-safe configuration helpers for creating enhanced query and mutation options in @tanstack/react-query.

3

4

## Overview

5

6

Configuration helpers provide type-safe ways to create query and mutation options with enhanced TypeScript inference. These helpers are particularly useful for creating reusable query configurations and ensuring proper type safety across your application.

7

8

## Query Configuration

9

10

### queryOptions

11

12

Creates type-safe query options with enhanced TypeScript data tagging for better type inference.

13

14

```typescript { .api }

15

function queryOptions<

16

TQueryFnData = unknown,

17

TError = DefaultError,

18

TData = TQueryFnData,

19

TQueryKey extends QueryKey = QueryKey

20

>(

21

options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>

22

): UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {

23

queryKey: TQueryKey

24

}

25

```

26

27

**Parameters:**

28

- `options`: `UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>` - Query configuration object

29

30

**Returns:** Enhanced query options with improved TypeScript inference

31

32

**Key Features:**

33

- Enhanced type inference for query data

34

- Type tagging for better IDE support

35

- Supports all standard query options

36

- Multiple overloads for different initial data scenarios

37

38

**Example:**

39

```typescript

40

import { queryOptions, useQuery } from '@tanstack/react-query'

41

42

// Define reusable query options

43

const userQueryOptions = (userId: string) =>

44

queryOptions({

45

queryKey: ['users', userId],

46

queryFn: async () => {

47

const response = await fetch(`/api/users/${userId}`)

48

return response.json() as Promise<User>

49

},

50

staleTime: 5 * 60 * 1000, // 5 minutes

51

gcTime: 10 * 60 * 1000, // 10 minutes

52

})

53

54

// Use in components with full type safety

55

function UserProfile({ userId }: { userId: string }) {

56

const options = userQueryOptions(userId)

57

const { data: user } = useQuery(options)

58

59

return <div>{user?.name}</div> // user is properly typed as User | undefined

60

}

61

62

// With defined initial data

63

const userWithInitialDataOptions = (userId: string, initialUser: User) =>

64

queryOptions({

65

queryKey: ['users', userId],

66

queryFn: () => fetchUser(userId),

67

initialData: initialUser, // Ensures data is always defined

68

})

69

70

function UserProfileWithInitialData({ userId, initialUser }: {

71

userId: string

72

initialUser: User

73

}) {

74

const { data: user } = useQuery(

75

userWithInitialDataOptions(userId, initialUser)

76

)

77

78

return <div>{user.name}</div> // user is typed as User (never undefined)

79

}

80

81

// Shared options across multiple components

82

export const postsQueryOptions = {

83

all: () =>

84

queryOptions({

85

queryKey: ['posts'],

86

queryFn: fetchAllPosts,

87

}),

88

89

byCategory: (category: string) =>

90

queryOptions({

91

queryKey: ['posts', { category }],

92

queryFn: () => fetchPostsByCategory(category),

93

}),

94

95

byId: (postId: string) =>

96

queryOptions({

97

queryKey: ['posts', postId],

98

queryFn: () => fetchPost(postId),

99

staleTime: Infinity, // Posts don't change often

100

}),

101

}

102

```

103

104

### infiniteQueryOptions

105

106

Creates type-safe infinite query options with enhanced TypeScript data tagging.

107

108

```typescript { .api }

109

function infiniteQueryOptions<

110

TQueryFnData = unknown,

111

TError = DefaultError,

112

TData = TQueryFnData,

113

TQueryKey extends QueryKey = QueryKey,

114

TPageParam = unknown

115

>(

116

options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>

117

): UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {

118

queryKey: TQueryKey

119

}

120

```

121

122

**Parameters:**

123

- `options`: `UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>` - Infinite query configuration

124

125

**Returns:** Enhanced infinite query options with improved TypeScript inference

126

127

**Key Features:**

128

- Type-safe pagination parameter handling

129

- Enhanced inference for page data structure

130

- Support for both forward and backward pagination

131

- Type tagging for better IDE support

132

133

**Example:**

134

```typescript

135

import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'

136

137

// Reusable infinite query options

138

const postsInfiniteOptions = (category?: string) =>

139

infiniteQueryOptions({

140

queryKey: ['posts', 'infinite', { category }],

141

queryFn: ({ pageParam = 0 }) =>

142

fetchPosts({

143

page: pageParam,

144

category,

145

limit: 10

146

}),

147

getNextPageParam: (lastPage, allPages) => {

148

return lastPage.hasMore ? allPages.length : undefined

149

},

150

getPreviousPageParam: (firstPage, allPages) => {

151

return allPages.length > 1 ? 0 : undefined

152

},

153

initialPageParam: 0,

154

})

155

156

function InfinitePostsList({ category }: { category?: string }) {

157

const {

158

data,

159

fetchNextPage,

160

hasNextPage,

161

isFetchingNextPage,

162

} = useInfiniteQuery(postsInfiniteOptions(category))

163

164

return (

165

<div>

166

{data?.pages.map((page, i) => (

167

<div key={i}>

168

{page.posts.map((post) => (

169

<PostCard key={post.id} post={post} />

170

))}

171

</div>

172

))}

173

174

{hasNextPage && (

175

<button

176

onClick={() => fetchNextPage()}

177

disabled={isFetchingNextPage}

178

>

179

{isFetchingNextPage ? 'Loading...' : 'Load More'}

180

</button>

181

)}

182

</div>

183

)

184

}

185

186

// Cursor-based pagination

187

const cursorBasedPostsOptions = () =>

188

infiniteQueryOptions({

189

queryKey: ['posts', 'cursor-based'],

190

queryFn: ({ pageParam }) =>

191

fetchPostsWithCursor({ cursor: pageParam }),

192

getNextPageParam: (lastPage) => lastPage.nextCursor,

193

initialPageParam: undefined as string | undefined,

194

})

195

196

// Bidirectional infinite queries

197

const bidirectionalMessagesOptions = (channelId: string) =>

198

infiniteQueryOptions({

199

queryKey: ['messages', channelId],

200

queryFn: ({ pageParam = { direction: 'newer', cursor: null } }) =>

201

fetchMessages(channelId, pageParam),

202

getNextPageParam: (lastPage) => ({

203

direction: 'newer' as const,

204

cursor: lastPage.newestCursor,

205

}),

206

getPreviousPageParam: (firstPage) => ({

207

direction: 'older' as const,

208

cursor: firstPage.oldestCursor,

209

}),

210

initialPageParam: { direction: 'newer' as const, cursor: null },

211

})

212

```

213

214

## Mutation Configuration

215

216

### mutationOptions

217

218

Creates type-safe mutation options for consistent mutation configuration.

219

220

```typescript { .api }

221

function mutationOptions<

222

TData = unknown,

223

TError = DefaultError,

224

TVariables = void,

225

TContext = unknown

226

>(

227

options: UseMutationOptions<TData, TError, TVariables, TContext>

228

): UseMutationOptions<TData, TError, TVariables, TContext>

229

```

230

231

**Parameters:**

232

- `options`: `UseMutationOptions<TData, TError, TVariables, TContext>` - Mutation configuration object

233

234

**Returns:** Same mutation options object with enhanced type safety

235

236

**Key Features:**

237

- Type-safe mutation variable handling

238

- Enhanced context typing for optimistic updates

239

- Support for both keyed and unkeyed mutations

240

- Consistent error handling patterns

241

242

**Example:**

243

```typescript

244

import { mutationOptions, useMutation, useQueryClient } from '@tanstack/react-query'

245

246

// Define reusable mutation options

247

const createUserMutationOptions = () =>

248

mutationOptions({

249

mutationFn: async (userData: CreateUserRequest) => {

250

const response = await fetch('/api/users', {

251

method: 'POST',

252

headers: { 'Content-Type': 'application/json' },

253

body: JSON.stringify(userData),

254

})

255

return response.json() as Promise<User>

256

},

257

onSuccess: (newUser, variables, context) => {

258

// newUser is typed as User

259

// variables is typed as CreateUserRequest

260

console.log('User created:', newUser.name)

261

},

262

onError: (error, variables, context) => {

263

// Error handling with proper typing

264

console.error('Failed to create user:', error.message)

265

},

266

})

267

268

function CreateUserForm() {

269

const createUserMutation = useMutation(createUserMutationOptions())

270

271

const handleSubmit = (userData: CreateUserRequest) => {

272

createUserMutation.mutate(userData)

273

}

274

275

return (

276

<form onSubmit={(e) => {

277

e.preventDefault()

278

const formData = new FormData(e.currentTarget)

279

handleSubmit({

280

name: formData.get('name') as string,

281

email: formData.get('email') as string,

282

})

283

}}>

284

<input name="name" placeholder="Name" required />

285

<input name="email" type="email" placeholder="Email" required />

286

<button

287

type="submit"

288

disabled={createUserMutation.isPending}

289

>

290

{createUserMutation.isPending ? 'Creating...' : 'Create User'}

291

</button>

292

</form>

293

)

294

}

295

296

// Mutation with optimistic updates

297

const updateUserMutationOptions = () => {

298

const queryClient = useQueryClient()

299

300

return mutationOptions({

301

mutationKey: ['updateUser'],

302

mutationFn: async ({ userId, updates }: {

303

userId: string

304

updates: Partial<User>

305

}) => {

306

const response = await fetch(`/api/users/${userId}`, {

307

method: 'PATCH',

308

headers: { 'Content-Type': 'application/json' },

309

body: JSON.stringify(updates),

310

})

311

return response.json() as Promise<User>

312

},

313

314

// Optimistic update

315

onMutate: async ({ userId, updates }) => {

316

await queryClient.cancelQueries({ queryKey: ['users', userId] })

317

318

const previousUser = queryClient.getQueryData(['users', userId])

319

320

queryClient.setQueryData(['users', userId], (old: User) => ({

321

...old,

322

...updates,

323

}))

324

325

return { previousUser }

326

},

327

328

// Rollback on error

329

onError: (error, variables, context) => {

330

if (context?.previousUser) {

331

queryClient.setQueryData(

332

['users', variables.userId],

333

context.previousUser

334

)

335

}

336

},

337

338

// Refetch on success or error

339

onSettled: (data, error, variables) => {

340

queryClient.invalidateQueries({

341

queryKey: ['users', variables.userId]

342

})

343

},

344

})

345

}

346

347

// Keyed mutations for tracking multiple instances

348

const deletePostMutationOptions = (postId: string) =>

349

mutationOptions({

350

mutationKey: ['deletePost', postId],

351

mutationFn: async () => {

352

await fetch(`/api/posts/${postId}`, { method: 'DELETE' })

353

},

354

onSuccess: () => {

355

// Invalidate and refetch posts list

356

queryClient.invalidateQueries({ queryKey: ['posts'] })

357

},

358

})

359

360

// Batch mutation options

361

const batchUpdateMutationOptions = () =>

362

mutationOptions({

363

mutationKey: ['batchUpdate'],

364

mutationFn: async (updates: Array<{ id: string; data: Partial<User> }>) => {

365

const promises = updates.map(({ id, data }) =>

366

fetch(`/api/users/${id}`, {

367

method: 'PATCH',

368

headers: { 'Content-Type': 'application/json' },

369

body: JSON.stringify(data),

370

}).then(res => res.json())

371

)

372

return Promise.all(promises) as Promise<User[]>

373

},

374

onSuccess: (updatedUsers) => {

375

// Update cache for each user

376

updatedUsers.forEach(user => {

377

queryClient.setQueryData(['users', user.id], user)

378

})

379

},

380

})

381

```

382

383

## Advanced Configuration Patterns

384

385

### Factory Functions

386

387

Create configuration factories for common patterns:

388

389

```typescript

390

import { queryOptions, infiniteQueryOptions, mutationOptions } from '@tanstack/react-query'

391

392

// Generic CRUD query factory

393

function createCrudOptions<T>(resource: string) {

394

return {

395

list: () =>

396

queryOptions({

397

queryKey: [resource],

398

queryFn: () => fetchResource<T[]>(`/api/${resource}`),

399

}),

400

401

byId: (id: string) =>

402

queryOptions({

403

queryKey: [resource, id],

404

queryFn: () => fetchResource<T>(`/api/${resource}/${id}`),

405

}),

406

407

infinite: (filters?: Record<string, any>) =>

408

infiniteQueryOptions({

409

queryKey: [resource, 'infinite', filters],

410

queryFn: ({ pageParam = 0 }) =>

411

fetchResource<PaginatedResponse<T>>(`/api/${resource}`, {

412

page: pageParam,

413

...filters,

414

}),

415

getNextPageParam: (lastPage) => lastPage.nextPage,

416

initialPageParam: 0,

417

}),

418

419

create: () =>

420

mutationOptions({

421

mutationFn: (data: Omit<T, 'id'>) =>

422

postResource<T>(`/api/${resource}`, data),

423

}),

424

425

update: () =>

426

mutationOptions({

427

mutationFn: ({ id, data }: { id: string; data: Partial<T> }) =>

428

patchResource<T>(`/api/${resource}/${id}`, data),

429

}),

430

431

delete: () =>

432

mutationOptions({

433

mutationFn: (id: string) =>

434

deleteResource(`/api/${resource}/${id}`),

435

}),

436

}

437

}

438

439

// Usage

440

const userOptions = createCrudOptions<User>('users')

441

const postOptions = createCrudOptions<Post>('posts')

442

443

// In components

444

const { data: users } = useQuery(userOptions.list())

445

const { data: user } = useQuery(userOptions.byId('123'))

446

const createUser = useMutation(userOptions.create())

447

```

448

449

### Configuration with Default Options

450

451

Set up default configurations for your application:

452

453

```typescript

454

// Define default options

455

const defaultQueryOptions = {

456

staleTime: 5 * 60 * 1000, // 5 minutes

457

gcTime: 10 * 60 * 1000, // 10 minutes

458

retry: 3,

459

retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),

460

}

461

462

const defaultMutationOptions = {

463

retry: 1,

464

retryDelay: 1000,

465

}

466

467

// Enhanced configuration helpers

468

const enhancedQueryOptions = <T extends UseQueryOptions>(options: T) =>

469

queryOptions({

470

...defaultQueryOptions,

471

...options,

472

})

473

474

const enhancedMutationOptions = <T extends UseMutationOptions>(options: T) =>

475

mutationOptions({

476

...defaultMutationOptions,

477

...options,

478

})

479

480

// Usage with defaults

481

const userQuery = enhancedQueryOptions({

482

queryKey: ['users', '123'],

483

queryFn: () => fetchUser('123'),

484

// Inherits all default options

485

})

486

```

487

488

## Type Definitions

489

490

```typescript { .api }

491

interface UseQueryOptions<

492

TQueryFnData = unknown,

493

TError = DefaultError,

494

TData = TQueryFnData,

495

TQueryKey extends QueryKey = QueryKey

496

> {

497

queryKey: TQueryKey

498

queryFn?: QueryFunction<TQueryFnData, TQueryKey>

499

enabled?: boolean

500

staleTime?: number

501

gcTime?: number

502

retry?: RetryValue<TError>

503

retryDelay?: RetryDelayValue<TError>

504

refetchOnMount?: boolean | 'always'

505

refetchOnWindowFocus?: boolean | 'always'

506

refetchOnReconnect?: boolean | 'always'

507

refetchInterval?: number | false

508

refetchIntervalInBackground?: boolean

509

initialData?: TData | InitialDataFunction<TData>

510

placeholderData?: TData | PlaceholderDataFunction<TData>

511

select?: (data: TQueryFnData) => TData

512

throwOnError?: ThrowOnError<TQueryFnData, TError, TData, TQueryKey>

513

// ... additional options

514

}

515

516

interface UseInfiniteQueryOptions<

517

TQueryFnData = unknown,

518

TError = DefaultError,

519

TData = TQueryFnData,

520

TQueryKey extends QueryKey = QueryKey,

521

TPageParam = unknown

522

> extends UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> {

523

getNextPageParam: GetNextPageParamFunction<TQueryFnData, TPageParam>

524

getPreviousPageParam?: GetPreviousPageParamFunction<TQueryFnData, TPageParam>

525

initialPageParam: TPageParam

526

maxPages?: number

527

}

528

529

interface UseMutationOptions<

530

TData = unknown,

531

TError = DefaultError,

532

TVariables = void,

533

TContext = unknown

534

> {

535

mutationKey?: MutationKey

536

mutationFn?: MutationFunction<TData, TVariables>

537

onMutate?: (variables: TVariables) => Promise<TContext | void> | TContext | void

538

onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => Promise<unknown> | unknown

539

onError?: (error: TError, variables: TVariables, context: TContext | undefined) => unknown

540

onSettled?: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => unknown

541

retry?: RetryValue<TError>

542

retryDelay?: RetryDelayValue<TError>

543

throwOnError?: ThrowOnError<TData, TError, TVariables, unknown>

544

// ... additional options

545

}

546

```

547

548

## Best Practices

549

550

1. **Use configuration helpers** for better type safety and code reuse

551

2. **Create factory functions** for common resource patterns

552

3. **Set up default options** at the application level

553

4. **Type your data models** explicitly for better inference

554

5. **Use keyed mutations** when tracking multiple instances

555

6. **Implement optimistic updates** for better user experience

556

7. **Handle errors consistently** across your application

557

558

These configuration helpers provide the foundation for building type-safe, maintainable query and mutation logic in your React Query applications.