or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdinfinite-queries.mdmulti-query-operations.mdmutation-management.mdoptions-helpers.mdprovider-setup.mdquery-management.mdstatus-monitoring.md

options-helpers.mddocs/

0

# Options Helpers

1

2

Type-safe utility functions for creating reusable query and mutation options with proper type inference and sharing across components.

3

4

## Capabilities

5

6

### Query Options

7

8

Type-safe helper for sharing and reusing query options with automatic type tagging.

9

10

```typescript { .api }

11

/**

12

* Allows to share and re-use query options in a type-safe way.

13

* The queryKey will be tagged with the type from queryFn.

14

* @param options - The query options to tag with the type from queryFn

15

* @returns The tagged query options

16

*/

17

function queryOptions<TQueryFnData, TError, TData, TQueryKey>(

18

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

19

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

20

queryKey: DataTag<TQueryKey, TQueryFnData, TError>;

21

};

22

23

// Overloads for different initial data scenarios

24

function queryOptions<TQueryFnData, TError, TData, TQueryKey>(

25

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

26

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

27

queryKey: DataTag<TQueryKey, TQueryFnData, TError>;

28

};

29

30

function queryOptions<TQueryFnData, TError, TData, TQueryKey>(

31

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

32

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

33

queryKey: DataTag<TQueryKey, TQueryFnData, TError>;

34

};

35

```

36

37

**Usage Examples:**

38

39

```typescript

40

import { queryOptions, injectQuery, QueryClient } from "@tanstack/angular-query-experimental";

41

import { Injectable, inject } from "@angular/core";

42

import { HttpClient } from "@angular/common/http";

43

44

// Define reusable query options

45

@Injectable({ providedIn: 'root' })

46

export class UserQueriesService {

47

#http = inject(HttpClient);

48

49

userById(id: number) {

50

return queryOptions({

51

queryKey: ['user', id] as const,

52

queryFn: () => this.#http.get<User>(`/api/users/${id}`),

53

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

54

retry: 1

55

});

56

}

57

58

userPosts(userId: number) {

59

return queryOptions({

60

queryKey: ['posts', 'user', userId] as const,

61

queryFn: () => this.#http.get<Post[]>(`/api/users/${userId}/posts`),

62

staleTime: 2 * 60 * 1000 // 2 minutes

63

});

64

}

65

66

currentUser() {

67

return queryOptions({

68

queryKey: ['user', 'current'] as const,

69

queryFn: () => this.#http.get<User>('/api/user/current'),

70

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

71

retry: 2

72

});

73

}

74

}

75

76

// Use in components

77

@Component({

78

selector: 'app-user-profile',

79

template: `

80

<div *ngIf="userQuery.isSuccess()">

81

<h1>{{ userQuery.data()?.name }}</h1>

82

<p>{{ userQuery.data()?.email }}</p>

83

</div>

84

85

<div *ngIf="postsQuery.isSuccess()">

86

<h2>Posts</h2>

87

<div *ngFor="let post of postsQuery.data()">

88

{{ post.title }}

89

</div>

90

</div>

91

`

92

})

93

export class UserProfileComponent {

94

#queries = inject(UserQueriesService);

95

#queryClient = inject(QueryClient);

96

97

userId = signal(1);

98

99

// Type-safe query usage

100

userQuery = injectQuery(() => this.#queries.userById(this.userId()));

101

postsQuery = injectQuery(() => this.#queries.userPosts(this.userId()));

102

103

// Can also use the tagged queryKey for manual operations

104

refreshUser() {

105

const options = this.#queries.userById(this.userId());

106

this.#queryClient.invalidateQueries({ queryKey: options.queryKey });

107

}

108

109

// Type inference works correctly

110

getUserData() {

111

const options = this.#queries.userById(this.userId());

112

return this.#queryClient.getQueryData(options.queryKey); // Type: User | undefined

113

}

114

}

115

116

interface User {

117

id: number;

118

name: string;

119

email: string;

120

}

121

122

interface Post {

123

id: number;

124

title: string;

125

content: string;

126

}

127

```

128

129

### Infinite Query Options

130

131

Type-safe helper for sharing and reusing infinite query options with automatic type tagging.

132

133

```typescript { .api }

134

/**

135

* Allows to share and re-use infinite query options in a type-safe way.

136

* The queryKey will be tagged with the type from queryFn.

137

* @param options - The infinite query options to tag with the type from queryFn

138

* @returns The tagged infinite query options

139

*/

140

function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(

141

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

142

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

143

queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;

144

};

145

146

// Overloads for different initial data scenarios

147

function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(

148

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

149

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

150

queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;

151

};

152

153

function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(

154

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

155

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

156

queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;

157

};

158

```

159

160

**Usage Examples:**

161

162

```typescript

163

import { infiniteQueryOptions, injectInfiniteQuery } from "@tanstack/angular-query-experimental";

164

165

@Injectable({ providedIn: 'root' })

166

export class PostQueriesService {

167

#http = inject(HttpClient);

168

169

allPosts() {

170

return infiniteQueryOptions({

171

queryKey: ['posts', 'infinite'] as const,

172

queryFn: ({ pageParam = 1 }) =>

173

this.#http.get<PostsPage>(`/api/posts?page=${pageParam}&limit=10`),

174

initialPageParam: 1,

175

getNextPageParam: (lastPage) =>

176

lastPage.hasMore ? lastPage.nextPage : null,

177

staleTime: 5 * 60 * 1000

178

});

179

}

180

181

postsByCategory(category: string) {

182

return infiniteQueryOptions({

183

queryKey: ['posts', 'category', category] as const,

184

queryFn: ({ pageParam = 1 }) =>

185

this.#http.get<PostsPage>(`/api/posts?category=${category}&page=${pageParam}`),

186

initialPageParam: 1,

187

getNextPageParam: (lastPage) =>

188

lastPage.hasMore ? lastPage.nextPage : null

189

});

190

}

191

}

192

193

@Component({})

194

export class PostsListComponent {

195

#queries = inject(PostQueriesService);

196

197

category = signal('technology');

198

199

postsQuery = injectInfiniteQuery(() =>

200

this.#queries.postsByCategory(this.category())

201

);

202

}

203

```

204

205

### Mutation Options

206

207

Type-safe helper for sharing and reusing mutation options.

208

209

```typescript { .api }

210

/**

211

* Allows to share and re-use mutation options in a type-safe way.

212

* @param options - The mutation options

213

* @returns Mutation options

214

*/

215

function mutationOptions<TData, TError, TVariables, TContext>(

216

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

217

): CreateMutationOptions<TData, TError, TVariables, TContext>;

218

219

// Overload for mutations with mutation key

220

function mutationOptions<TData, TError, TVariables, TContext>(

221

options: WithRequired<CreateMutationOptions<TData, TError, TVariables, TContext>, 'mutationKey'>

222

): WithRequired<CreateMutationOptions<TData, TError, TVariables, TContext>, 'mutationKey'>;

223

224

// Overload for mutations without mutation key

225

function mutationOptions<TData, TError, TVariables, TContext>(

226

options: Omit<CreateMutationOptions<TData, TError, TVariables, TContext>, 'mutationKey'>

227

): Omit<CreateMutationOptions<TData, TError, TVariables, TContext>, 'mutationKey'>;

228

```

229

230

**Usage Examples:**

231

232

```typescript

233

import { mutationOptions, injectMutation, QueryClient } from "@tanstack/angular-query-experimental";

234

235

@Injectable({ providedIn: 'root' })

236

export class UserMutationsService {

237

#http = inject(HttpClient);

238

#queryClient = inject(QueryClient);

239

240

createUser() {

241

return mutationOptions({

242

mutationKey: ['user', 'create'] as const,

243

mutationFn: (userData: CreateUserRequest) =>

244

this.#http.post<User>('/api/users', userData),

245

onSuccess: (newUser) => {

246

// Invalidate user lists

247

this.#queryClient.invalidateQueries({ queryKey: ['users'] });

248

// Set individual user data

249

this.#queryClient.setQueryData(['user', newUser.id], newUser);

250

},

251

onError: (error) => {

252

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

253

}

254

});

255

}

256

257

updateUser(id: number) {

258

return mutationOptions({

259

mutationKey: ['user', 'update', id] as const,

260

mutationFn: (updates: Partial<User>) =>

261

this.#http.patch<User>(`/api/users/${id}`, updates),

262

onMutate: async (variables) => {

263

// Optimistic update

264

await this.#queryClient.cancelQueries({ queryKey: ['user', id] });

265

const previousUser = this.#queryClient.getQueryData<User>(['user', id]);

266

267

this.#queryClient.setQueryData(['user', id], (old: User) => ({

268

...old,

269

...variables

270

}));

271

272

return { previousUser };

273

},

274

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

275

// Rollback on error

276

if (context?.previousUser) {

277

this.#queryClient.setQueryData(['user', id], context.previousUser);

278

}

279

},

280

onSettled: () => {

281

this.#queryClient.invalidateQueries({ queryKey: ['user', id] });

282

}

283

});

284

}

285

286

deleteUser(id: number) {

287

return mutationOptions({

288

mutationKey: ['user', 'delete', id] as const,

289

mutationFn: () => this.#http.delete(`/api/users/${id}`),

290

onSuccess: () => {

291

// Remove from cache

292

this.#queryClient.removeQueries({ queryKey: ['user', id] });

293

// Invalidate user lists

294

this.#queryClient.invalidateQueries({ queryKey: ['users'] });

295

}

296

});

297

}

298

}

299

300

@Component({

301

selector: 'app-user-form',

302

template: `

303

<form (ngSubmit)="handleSubmit()">

304

<input [(ngModel)]="name" placeholder="Name" />

305

<input [(ngModel)]="email" placeholder="Email" />

306

307

<button

308

type="submit"

309

[disabled]="createMutation.isPending()"

310

>

311

{{ createMutation.isPending() ? 'Creating...' : 'Create User' }}

312

</button>

313

314

<button

315

*ngIf="editingUser()"

316

type="button"

317

(click)="handleUpdate()"

318

[disabled]="updateMutation.isPending()"

319

>

320

Update

321

</button>

322

</form>

323

`

324

})

325

export class UserFormComponent {

326

#mutations = inject(UserMutationsService);

327

328

name = '';

329

email = '';

330

editingUser = signal<User | null>(null);

331

332

createMutation = injectMutation(() => this.#mutations.createUser());

333

334

updateMutation = injectMutation(() => {

335

const user = this.editingUser();

336

return user ? this.#mutations.updateUser(user.id) : this.#mutations.createUser();

337

});

338

339

handleSubmit() {

340

this.createMutation.mutate({

341

name: this.name,

342

email: this.email

343

});

344

}

345

346

handleUpdate() {

347

this.updateMutation.mutate({

348

name: this.name,

349

email: this.email

350

});

351

}

352

}

353

```

354

355

## Advanced Usage Patterns

356

357

### Factory Functions with Parameters

358

359

```typescript

360

@Injectable({ providedIn: 'root' })

361

export class DataQueriesService {

362

#http = inject(HttpClient);

363

364

// Factory function for paginated data

365

paginatedData<T>(

366

endpoint: string,

367

config: { pageSize?: number; staleTime?: number } = {}

368

) {

369

const { pageSize = 20, staleTime = 5 * 60 * 1000 } = config;

370

371

return queryOptions({

372

queryKey: ['paginated', endpoint, { pageSize }] as const,

373

queryFn: ({ pageParam = 1 }) =>

374

this.#http.get<PaginatedResponse<T>>(`${endpoint}?page=${pageParam}&limit=${pageSize}`),

375

staleTime

376

});

377

}

378

379

// Factory for filtered queries

380

filteredQuery<T>(

381

endpoint: string,

382

filters: Record<string, any>,

383

config: { staleTime?: number } = {}

384

) {

385

return queryOptions({

386

queryKey: ['filtered', endpoint, filters] as const,

387

queryFn: () => {

388

const params = new URLSearchParams();

389

Object.entries(filters).forEach(([key, value]) => {

390

if (value != null) params.set(key, String(value));

391

});

392

return this.#http.get<T>(`${endpoint}?${params}`);

393

},

394

staleTime: config.staleTime || 2 * 60 * 1000

395

});

396

}

397

}

398

399

// Usage

400

@Component({})

401

export class ProductsComponent {

402

#queries = inject(DataQueriesService);

403

404

searchFilters = signal({ category: 'electronics', minPrice: 100 });

405

406

productsQuery = injectQuery(() =>

407

this.#queries.filteredQuery<Product[]>('/api/products', this.searchFilters())

408

);

409

}

410

```

411

412

### Conditional Options

413

414

```typescript

415

@Injectable({ providedIn: 'root' })

416

export class ConditionalQueriesService {

417

#http = inject(HttpClient);

418

419

userData(userId: number, includePrivate: boolean = false) {

420

return queryOptions({

421

queryKey: ['user', userId, { includePrivate }] as const,

422

queryFn: () => {

423

const endpoint = includePrivate

424

? `/api/users/${userId}/private`

425

: `/api/users/${userId}`;

426

return this.#http.get<User>(endpoint);

427

},

428

staleTime: includePrivate ? 1 * 60 * 1000 : 10 * 60 * 1000, // Private data stales faster

429

retry: includePrivate ? 0 : 1 // Don't retry private data requests

430

});

431

}

432

433

searchData(query: string, options: SearchOptions = {}) {

434

return queryOptions({

435

queryKey: ['search', query, options] as const,

436

queryFn: () => this.#http.post<SearchResult[]>('/api/search', { query, ...options }),

437

enabled: query.length > 2,

438

staleTime: options.realtime ? 0 : 5 * 60 * 1000

439

});

440

}

441

}

442

```

443

444

### Composition Patterns

445

446

```typescript

447

@Injectable({ providedIn: 'root' })

448

export class ComposedQueriesService {

449

#http = inject(HttpClient);

450

451

// Base query options

452

private baseQueryOptions = {

453

staleTime: 5 * 60 * 1000,

454

retry: 1,

455

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

456

};

457

458

// Composed with additional options

459

criticalData(id: string) {

460

return queryOptions({

461

...this.baseQueryOptions,

462

queryKey: ['critical', id] as const,

463

queryFn: () => this.#http.get<CriticalData>(`/api/critical/${id}`),

464

retry: 3, // Override base retry

465

refetchOnWindowFocus: true

466

});

467

}

468

469

// Composed for real-time data

470

realtimeData(channel: string) {

471

return queryOptions({

472

...this.baseQueryOptions,

473

queryKey: ['realtime', channel] as const,

474

queryFn: () => this.#http.get<RealtimeData>(`/api/realtime/${channel}`),

475

staleTime: 0, // Override base staleTime

476

refetchInterval: 5000

477

});

478

}

479

}

480

```

481

482

### Testing Helpers

483

484

```typescript

485

// test-queries.service.ts

486

@Injectable()

487

export class TestQueriesService {

488

mockUserById(id: number, userData: User) {

489

return queryOptions({

490

queryKey: ['user', id] as const,

491

queryFn: () => Promise.resolve(userData),

492

staleTime: Infinity // Never stale in tests

493

});

494

}

495

496

mockFailingQuery<T>(errorMessage: string) {

497

return queryOptions({

498

queryKey: ['failing'] as const,

499

queryFn: (): Promise<T> => Promise.reject(new Error(errorMessage)),

500

retry: false

501

});

502

}

503

}

504

505

// In tests

506

describe('UserComponent', () => {

507

let testQueries: TestQueriesService;

508

509

beforeEach(() => {

510

testQueries = TestBed.inject(TestQueriesService);

511

});

512

513

it('should display user data', () => {

514

const mockUser = { id: 1, name: 'Test User', email: 'test@example.com' };

515

516

// Use mock query options

517

component.userQuery = injectQuery(() => testQueries.mockUserById(1, mockUser));

518

519

expect(component.userQuery.data()).toEqual(mockUser);

520

});

521

});

522

```