@ametie/vue-muza-use 0.9.1 โ 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +371 -174
- package/dist/index.cjs +133 -47
- package/dist/index.d.cts +149 -21
- package/dist/index.d.ts +149 -21
- package/dist/index.mjs +117 -31
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,29 +5,71 @@
|
|
|
5
5
|
[](https://vuejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**TypeScript-first, feature-rich Axios wrapper for Vue 3 Composition API. Built for real-world business logic.**
|
|
9
9
|
|
|
10
10
|
A production-ready composable that eliminates boilerplate and solves the hard problems: race conditions, token refresh queues, automatic retries, and reactive request management. Write less code, ship faster, sleep better.
|
|
11
11
|
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> ### ๐ค Claude Code โ Built-in AI Skill
|
|
14
|
+
>
|
|
15
|
+
> This library ships with a skill that teaches Claude the feature wrapper pattern, naming conventions, and all `UseApiOptions`.
|
|
16
|
+
> Claude will generate correct, architecture-consistent API layer code out of the box โ no extra prompting needed.
|
|
17
|
+
>
|
|
18
|
+
> ๐ **[View skill file โ](https://github.com/MortyQ/vue-muza-use/blob/main/.claude/skills/use-api/SKILL.md)**
|
|
19
|
+
|
|
12
20
|
---
|
|
13
21
|
|
|
22
|
+
> **Using v0.x?** The legacy documentation is available at [v0.10.0 README](https://github.com/MortyQ/vue-muza-use/blob/v0.10.0/packages/use-api/README.md).
|
|
23
|
+
|
|
14
24
|
## โจ Features
|
|
15
25
|
|
|
16
26
|
**Core Features** (Get started in minutes):
|
|
17
|
-
- ๐ฏ **
|
|
18
|
-
- ๐ **Smart Reactivity** โ
|
|
27
|
+
- ๐ฏ **TypeScript-first** โ Full TypeScript support with strict typing for requests and responses
|
|
28
|
+
- ๐ **Smart Reactivity** โ Auto-tracks reactive deps in `url`, `params`, and `data` โ refetches automatically when they change
|
|
19
29
|
- โฑ๏ธ **Built-in Debouncing** โ Perfect for search inputs and auto-save forms
|
|
20
30
|
- ๐ก๏ธ **Race Condition Protection** โ Global abort controller cancels stale requests automatically
|
|
21
31
|
- ๐ **Auto-Polling** โ Built-in interval fetching with smart tab visibility detection
|
|
22
32
|
- ๐ **Batch Requests** โ Execute multiple requests in parallel with progress tracking
|
|
23
33
|
- ๐งน **Zero Memory Leaks** โ Automatic cleanup of pending requests on component unmount
|
|
24
|
-
- ๐ **ignoreUpdates** โ
|
|
34
|
+
- ๐ **ignoreUpdates** โ Update reactive deps silently without triggering a re-fetch
|
|
25
35
|
- ๐๏ธ **Response Caching** โ In-memory cache with configurable TTL and manual invalidation
|
|
36
|
+
- โก **Stale-While-Revalidate** โ Serve cached data instantly while refreshing silently in the background
|
|
37
|
+
- ๐ฌ **select** โ Transform or filter response data declaratively; re-applied on every fetch automatically
|
|
26
38
|
|
|
27
39
|
**Advanced Features** (When you need them):
|
|
28
40
|
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic with configurable status codes
|
|
29
41
|
- ๐ **JWT Token Management** โ Automatic token refresh with request queueing on 401 responses
|
|
30
42
|
- ๐๏ธ **Flexible Architecture** โ Bring your own Axios instance with full configuration control
|
|
43
|
+
- ๐ช **withCredentials** โ Per-request cookie and cross-origin credential control
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ๐ How it compares
|
|
48
|
+
|
|
49
|
+
> Honest comparison. โ
built-in ยท โ ๏ธ partial or plugin needed ยท โ not supported
|
|
50
|
+
|
|
51
|
+
| Feature | vue-muza-use | @vueuse/useFetch | TanStack Query | swrv |
|
|
52
|
+
|---------|:---:|:---:|:---:|:---:|
|
|
53
|
+
| **Axios-first** | โ
| โ fetch | โ ๏ธ adapter | โ fetch |
|
|
54
|
+
| **JWT auto-refresh + queue** | โ
| โ | โ | โ |
|
|
55
|
+
| **Race condition protection** | โ
| โ | โ
| โ |
|
|
56
|
+
| **ignoreUpdates** | โ
| โ | โ | โ |
|
|
57
|
+
| **Built-in debounce** | โ
| โ | โ | โ |
|
|
58
|
+
| **Batch requests** | โ
| โ | โ | โ |
|
|
59
|
+
| **Built-in retry** | โ
| โ | โ
| โ |
|
|
60
|
+
| **Auto-polling** | โ
| โ | โ
| โ
|
|
|
61
|
+
| **SWR (stale-while-revalidate)** | โ
| โ | โ
| โ
|
|
|
62
|
+
| **select / transform** | โ
| โ | โ
| โ |
|
|
63
|
+
| **Response caching** | โ
| โ | โ
| โ
|
|
|
64
|
+
| **TypeScript** | โ
| โ
| โ
| โ
|
|
|
65
|
+
| **SSR / Nuxt** | โ | โ
| โ
| โ
|
|
|
66
|
+
| **DevTools** | โ | โ | โ
| โ |
|
|
67
|
+
|
|
68
|
+
**Choose vue-muza-use if:** you build Vue 3 SPAs with Axios, need JWT token refresh out of the box, and want reactive request management without a heavyweight server-state solution.
|
|
69
|
+
|
|
70
|
+
**Choose TanStack Query if:** you need SSR, DevTools, or advanced server-state normalization.
|
|
71
|
+
|
|
72
|
+
**Choose @vueuse/useFetch if:** you want a minimal fetch wrapper with no opinions.
|
|
31
73
|
|
|
32
74
|
---
|
|
33
75
|
|
|
@@ -40,13 +82,15 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
40
82
|
|
|
41
83
|
**Core Features:**
|
|
42
84
|
- [Watch & Auto-Refetch](#watch--auto-refetch)
|
|
43
|
-
- [ignoreUpdates โ
|
|
85
|
+
- [ignoreUpdates โ Update Without Re-fetching](#ignoreupdates--update-without-re-fetching)
|
|
44
86
|
- [Response Caching](#response-caching)
|
|
87
|
+
- [Stale-While-Revalidate (SWR)](#stale-while-revalidate-swr)
|
|
45
88
|
- [Polling (Background Updates)](#polling-background-updates)
|
|
46
89
|
- [Error Handling](#error-handling)
|
|
47
90
|
- [retry โ Automatic Request Retry](#retry--automatic-request-retry)
|
|
48
91
|
- [Loading States](#loading-states)
|
|
49
92
|
- [Manual Data Updates (mutate)](#manual-data-updates-mutate)
|
|
93
|
+
- [select โ Declarative Data Transformation](#select--declarative-data-transformation)
|
|
50
94
|
|
|
51
95
|
**Real-World Examples:**
|
|
52
96
|
- [Data Table with Pagination](#data-table-with-pagination--sorting)
|
|
@@ -154,7 +198,6 @@ const searchQuery = ref('')
|
|
|
154
198
|
const { data, loading } = useApi<Product[]>(
|
|
155
199
|
() => `/products/search?q=${searchQuery.value}`,
|
|
156
200
|
{
|
|
157
|
-
watch: searchQuery,
|
|
158
201
|
debounce: 500
|
|
159
202
|
}
|
|
160
203
|
)
|
|
@@ -257,7 +300,6 @@ const filters = ref({
|
|
|
257
300
|
|
|
258
301
|
const { data } = useApi('/users', {
|
|
259
302
|
params: filters,
|
|
260
|
-
watch: filters,
|
|
261
303
|
immediate: true
|
|
262
304
|
})
|
|
263
305
|
```
|
|
@@ -279,7 +321,6 @@ const id = ref<number | null>(null)
|
|
|
279
321
|
|
|
280
322
|
const { data } = useApi<User>(
|
|
281
323
|
() => id.value ? `/users/${id.value}` : undefined,
|
|
282
|
-
{ watch: id }
|
|
283
324
|
)
|
|
284
325
|
|
|
285
326
|
// No request fires until id.value is set
|
|
@@ -342,162 +383,130 @@ const { execute, loading, error } = useApi<LoginResponse>(
|
|
|
342
383
|
|
|
343
384
|
### Watch & Auto-Refetch
|
|
344
385
|
|
|
345
|
-
|
|
386
|
+
**TL;DR: Pass reactive refs or getters to `url`, `params`, or `data` โ the request re-fires automatically when they change.**
|
|
387
|
+
|
|
388
|
+
Any reactive dependency accessed inside a getter is tracked automatically. No explicit `watch` option needed.
|
|
346
389
|
|
|
347
|
-
#### Single Dependency
|
|
348
390
|
```typescript
|
|
349
391
|
import { ref } from 'vue'
|
|
350
392
|
import { useApi } from '@ametie/vue-muza-use'
|
|
351
393
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
name: string
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const userId = ref(1)
|
|
394
|
+
const search = ref('')
|
|
395
|
+
const page = ref(1)
|
|
358
396
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
{
|
|
362
|
-
|
|
397
|
+
// Reactive params getter โ both search and page are tracked automatically
|
|
398
|
+
const { data, loading } = useApi('/products', {
|
|
399
|
+
params: () => ({ q: search.value, page: page.value }),
|
|
400
|
+
immediate: true,
|
|
401
|
+
})
|
|
363
402
|
|
|
364
|
-
|
|
403
|
+
// Change any dep โ request re-fires automatically
|
|
404
|
+
search.value = 'shoes'
|
|
405
|
+
page.value = 2
|
|
365
406
|
```
|
|
366
407
|
|
|
367
|
-
|
|
408
|
+
**Reactive URL:**
|
|
368
409
|
```typescript
|
|
369
|
-
|
|
370
|
-
import { useApi } from '@ametie/vue-muza-use'
|
|
410
|
+
const userId = ref(1)
|
|
371
411
|
|
|
372
|
-
const
|
|
373
|
-
|
|
412
|
+
const { data } = useApi(() => `/users/${userId.value}`, {
|
|
413
|
+
immediate: true,
|
|
414
|
+
})
|
|
374
415
|
|
|
375
|
-
|
|
376
|
-
() => `/products?q=${searchQuery.value}&category=${category.value}`,
|
|
377
|
-
{
|
|
378
|
-
watch: [searchQuery, category],
|
|
379
|
-
debounce: 500
|
|
380
|
-
}
|
|
381
|
-
)
|
|
416
|
+
userId.value = 2 // โ re-fetches /users/2 automatically
|
|
382
417
|
```
|
|
383
418
|
|
|
384
|
-
|
|
419
|
+
**Opt-out with `lazy: true`:**
|
|
420
|
+
|
|
421
|
+
For forms and manual mutations where you control when `execute()` is called:
|
|
422
|
+
|
|
385
423
|
```typescript
|
|
386
|
-
|
|
387
|
-
import { useApi } from '@ametie/vue-muza-use'
|
|
424
|
+
const form = ref({ name: '', email: '' })
|
|
388
425
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
426
|
+
const { execute, loading } = useApi('/users', {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
data: form,
|
|
429
|
+
lazy: true, // form changes do NOT trigger re-fetch
|
|
430
|
+
onSuccess: () => router.push('/users'),
|
|
392
431
|
})
|
|
393
432
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
debounce: 1000,
|
|
399
|
-
onSuccess: () => console.log('Saved!')
|
|
400
|
-
})
|
|
433
|
+
// Only fires when you call it
|
|
434
|
+
async function submit() {
|
|
435
|
+
await execute()
|
|
436
|
+
}
|
|
401
437
|
```
|
|
402
438
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
### ignoreUpdates โ Atomic Updates Without Refetch
|
|
406
|
-
|
|
407
|
-
**TL;DR: Change multiple reactive values at once without triggering a request between each change.**
|
|
408
|
-
|
|
409
|
-
When `watch` is active, every change to a watched ref triggers a new request. If you need to update three filter fields at once, you'd get three requests instead of one. `ignoreUpdates` wraps your changes so the watcher stays silent while you update all fields, then you call `execute()` once.
|
|
410
|
-
|
|
411
|
-
#### โ Without ignoreUpdates โ 2 requests fire
|
|
439
|
+
**`immediate` works independently of auto-tracking:**
|
|
412
440
|
|
|
413
441
|
```typescript
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const search = ref('')
|
|
419
|
-
|
|
420
|
-
const { execute } = useApi('/users', {
|
|
421
|
-
params: { page, search },
|
|
422
|
-
watch: [page, search]
|
|
442
|
+
// Fetch on mount + re-fetch on dep change
|
|
443
|
+
useApi('/products', {
|
|
444
|
+
params: () => ({ q: search.value }),
|
|
445
|
+
immediate: true,
|
|
423
446
|
})
|
|
424
447
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
448
|
+
// No fetch on mount, but re-fetch on dep change
|
|
449
|
+
useApi('/products', {
|
|
450
|
+
params: () => ({ q: search.value }),
|
|
451
|
+
// immediate: false is the default
|
|
452
|
+
})
|
|
428
453
|
```
|
|
429
454
|
|
|
430
|
-
|
|
455
|
+
**Batching:** Vue batches synchronous reactive changes before triggering auto-tracking โ two synchronous ref changes fire only one request.
|
|
431
456
|
|
|
432
457
|
```typescript
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
// Only one request fires (Vue batches sync changes)
|
|
459
|
+
status.value = 'active'
|
|
460
|
+
page.value = 1
|
|
461
|
+
```
|
|
435
462
|
|
|
436
|
-
|
|
437
|
-
const search = ref('')
|
|
463
|
+
---
|
|
438
464
|
|
|
439
|
-
|
|
440
|
-
params: { page, search },
|
|
441
|
-
watch: [page, search]
|
|
442
|
-
})
|
|
465
|
+
### ignoreUpdates โ Update Without Re-fetching
|
|
443
466
|
|
|
444
|
-
|
|
445
|
-
ignoreUpdates(() => {
|
|
446
|
-
page.value = 1
|
|
447
|
-
search.value = 'john'
|
|
448
|
-
})
|
|
449
|
-
await execute() // โ single request: page=1, search=john
|
|
450
|
-
```
|
|
467
|
+
**TL;DR: Update a reactive dep without triggering auto-tracking.**
|
|
451
468
|
|
|
452
|
-
|
|
469
|
+
When auto-tracking is active, any reactive dep change fires a new request. `ignoreUpdates` pauses the tracking scope for the duration of the callback โ changes inside do not trigger a re-fetch.
|
|
453
470
|
|
|
454
|
-
|
|
471
|
+
#### Example โ clear search input without fetching
|
|
455
472
|
|
|
456
473
|
```typescript
|
|
457
474
|
import { ref } from 'vue'
|
|
458
475
|
import { useApi } from '@ametie/vue-muza-use'
|
|
459
476
|
|
|
460
|
-
const page = ref(1)
|
|
461
477
|
const search = ref('')
|
|
462
|
-
const status = ref('all')
|
|
463
478
|
|
|
464
|
-
const {
|
|
465
|
-
params: {
|
|
466
|
-
|
|
479
|
+
const { data, ignoreUpdates } = useApi('/products', {
|
|
480
|
+
params: () => ({ q: search.value }),
|
|
481
|
+
debounce: 300,
|
|
467
482
|
})
|
|
468
483
|
|
|
469
|
-
function
|
|
484
|
+
function clearSearch() {
|
|
470
485
|
ignoreUpdates(() => {
|
|
471
|
-
page.value = 1
|
|
472
486
|
search.value = ''
|
|
473
|
-
status.value = 'all'
|
|
474
487
|
})
|
|
475
|
-
|
|
488
|
+
// auto-tracking is paused โ no request fires
|
|
476
489
|
}
|
|
477
490
|
```
|
|
478
491
|
|
|
479
|
-
|
|
492
|
+
The user types โ auto-tracking fires โ debounced request. Clicking "Clear" resets the input without triggering a fetch.
|
|
480
493
|
|
|
481
|
-
|
|
494
|
+
#### Safe to call when `lazy: true`
|
|
482
495
|
|
|
483
|
-
|
|
484
|
-
import { ref } from 'vue'
|
|
485
|
-
import { useApi } from '@ametie/vue-muza-use'
|
|
496
|
+
If `lazy: true`, `ignoreUpdates` still runs the updater โ it just has nothing to suppress.
|
|
486
497
|
|
|
487
|
-
|
|
498
|
+
```typescript
|
|
488
499
|
const { ignoreUpdates } = useApi('/data')
|
|
489
500
|
|
|
490
|
-
// Safe โ no error thrown, updater still runs
|
|
491
501
|
ignoreUpdates(() => {
|
|
492
|
-
|
|
502
|
+
someRef.value = 42 // runs normally, nothing to suppress
|
|
493
503
|
})
|
|
494
504
|
```
|
|
495
505
|
|
|
496
506
|
> [!NOTE]
|
|
497
507
|
> `ignoreUpdates` is synchronous only. Changes made after an `await` inside the
|
|
498
508
|
> updater function will NOT be suppressed โ the flag resets after the synchronous
|
|
499
|
-
> portion completes.
|
|
500
|
-
> `ignoreUpdates` and call `execute()` manually.
|
|
509
|
+
> portion completes.
|
|
501
510
|
|
|
502
511
|
---
|
|
503
512
|
|
|
@@ -610,9 +619,9 @@ invalidateCache(['products', 'categories'])
|
|
|
610
619
|
clearAllCache()
|
|
611
620
|
```
|
|
612
621
|
|
|
613
|
-
#### cache +
|
|
622
|
+
#### cache + auto-tracking
|
|
614
623
|
|
|
615
|
-
When
|
|
624
|
+
When auto-tracking is active, each dep-change-triggered `execute()` still checks the cache first:
|
|
616
625
|
|
|
617
626
|
```vue
|
|
618
627
|
<script setup lang="ts">
|
|
@@ -623,7 +632,7 @@ const categoryId = ref<number>(1)
|
|
|
623
632
|
|
|
624
633
|
const { data } = useApi<Product[]>(() => `/categories/${categoryId.value}/products`, {
|
|
625
634
|
cache: { id: `products-cat-${categoryId.value}`, staleTime: 30_000 },
|
|
626
|
-
|
|
635
|
+
params: () => ({ category: categoryId.value }),
|
|
627
636
|
immediate: true,
|
|
628
637
|
})
|
|
629
638
|
</script>
|
|
@@ -660,6 +669,7 @@ const { data } = useApi('/reports', {
|
|
|
660
669
|
|-------|------|---------|-------------|
|
|
661
670
|
| `id` | `string` | โ | Unique cache key |
|
|
662
671
|
| `staleTime` | `number` | `300_000` | TTL in milliseconds. Entry is deleted on next read after this time |
|
|
672
|
+
| `swr` | `boolean` | `false` | Stale-while-revalidate: serve cached data instantly while revalidating in the background. See [SWR](#stale-while-revalidate-swr) |
|
|
663
673
|
|
|
664
674
|
#### Out of Scope (by design)
|
|
665
675
|
|
|
@@ -679,6 +689,68 @@ The following are intentionally **not** supported in v1:
|
|
|
679
689
|
|
|
680
690
|
---
|
|
681
691
|
|
|
692
|
+
### Stale-While-Revalidate (SWR)
|
|
693
|
+
|
|
694
|
+
**TL;DR: Return cached data instantly while fetching fresh data in the background. No loading spinner, no blank screen.**
|
|
695
|
+
|
|
696
|
+
Requires the `cache` option to be set. On a cache hit, the stale data is returned immediately (no `loading: true`, no spinner) while a silent background request runs. Use the `revalidating` ref to show a subtle refresh indicator if needed.
|
|
697
|
+
|
|
698
|
+
On a **cache miss** (first load), the request behaves exactly like a normal request โ `loading: true`, no stale data.
|
|
699
|
+
|
|
700
|
+
#### Basic Usage
|
|
701
|
+
|
|
702
|
+
```vue
|
|
703
|
+
<script setup lang="ts">
|
|
704
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
705
|
+
|
|
706
|
+
interface User { id: number; name: string }
|
|
707
|
+
|
|
708
|
+
const { data, revalidating } = useApi<User[]>('/users', {
|
|
709
|
+
cache: { id: 'users', swr: true },
|
|
710
|
+
immediate: true,
|
|
711
|
+
})
|
|
712
|
+
</script>
|
|
713
|
+
|
|
714
|
+
<template>
|
|
715
|
+
<!-- data renders immediately from cache โ no blank screen -->
|
|
716
|
+
<ul>
|
|
717
|
+
<li v-for="user in data" :key="user.id">
|
|
718
|
+
{{ user.name }}
|
|
719
|
+
<span v-if="revalidating">โป</span>
|
|
720
|
+
</li>
|
|
721
|
+
</ul>
|
|
722
|
+
</template>
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
#### SWR vs Normal Cache Hit
|
|
726
|
+
|
|
727
|
+
| | Normal cache hit | SWR cache hit |
|
|
728
|
+
|---|---|---|
|
|
729
|
+
| `loading` | `false` | `false` |
|
|
730
|
+
| `data` | Stale data, no new request | Stale data โ then fresh data |
|
|
731
|
+
| `revalidating` | `false` | `true` while fetching, then `false` |
|
|
732
|
+
| Axios request | **Not made** | **Made** (silent background fetch) |
|
|
733
|
+
| `onBefore` | Not called | **Not called** (silent) |
|
|
734
|
+
| `onSuccess` | Not called | **Called** with fresh response |
|
|
735
|
+
| `onFinish` | Not called | **Called** after background fetch |
|
|
736
|
+
|
|
737
|
+
#### Error Handling
|
|
738
|
+
|
|
739
|
+
If the background revalidation request fails:
|
|
740
|
+
- `revalidating` resets to `false`
|
|
741
|
+
- `error` is set
|
|
742
|
+
- The **stale data is preserved** โ your UI doesn't go blank
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
const { data, revalidating, error } = useApi('/dashboard', {
|
|
746
|
+
cache: { id: 'dashboard', swr: true },
|
|
747
|
+
immediate: true,
|
|
748
|
+
})
|
|
749
|
+
// data.value stays the cached value even after a failed revalidation
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
682
754
|
### Polling (Background Updates)
|
|
683
755
|
|
|
684
756
|
**TL;DR: Keep data fresh with smart polling that automatically pauses when the browser tab is hidden.**
|
|
@@ -922,7 +994,10 @@ const toggleTodo = (id: number) => {
|
|
|
922
994
|
}
|
|
923
995
|
```
|
|
924
996
|
|
|
925
|
-
#### Transform
|
|
997
|
+
#### Transform after fetch
|
|
998
|
+
|
|
999
|
+
Use `mutate` from the **same** `useApi` call to post-process data after it arrives:
|
|
1000
|
+
|
|
926
1001
|
```typescript
|
|
927
1002
|
import { useApi } from '@ametie/vue-muza-use'
|
|
928
1003
|
|
|
@@ -933,7 +1008,7 @@ interface User {
|
|
|
933
1008
|
fullName?: string
|
|
934
1009
|
}
|
|
935
1010
|
|
|
936
|
-
const { data } = useApi<User[]>('/users', {
|
|
1011
|
+
const { data, mutate } = useApi<User[]>('/users', {
|
|
937
1012
|
immediate: true,
|
|
938
1013
|
onSuccess: ({ data: users }) => {
|
|
939
1014
|
mutate(users.map(u => ({
|
|
@@ -942,10 +1017,80 @@ const { data } = useApi<User[]>('/users', {
|
|
|
942
1017
|
})))
|
|
943
1018
|
}
|
|
944
1019
|
})
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
> [!TIP]
|
|
1023
|
+
> If the same transformation runs on every fetch (including polling or watch re-triggers),
|
|
1024
|
+
> use [`select`](#select--declarative-data-transformation) instead โ it's applied automatically
|
|
1025
|
+
> and keeps your options object clean.
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
### select โ Declarative Data Transformation
|
|
945
1030
|
|
|
946
|
-
|
|
1031
|
+
**TL;DR: Transform, filter, or reshape response data once โ it's re-applied automatically on every fetch, polling tick, and SWR revalidation.**
|
|
1032
|
+
|
|
1033
|
+
Use `select` when you want the same transformation applied every time the request fires. Unlike `mutate` (which you call manually), `select` is declared once and runs silently on each response.
|
|
1034
|
+
|
|
1035
|
+
The second generic parameter of `useApi` controls the output type of `select`.
|
|
1036
|
+
|
|
1037
|
+
#### Extract a Nested Field
|
|
1038
|
+
|
|
1039
|
+
APIs that wrap responses in `{ data: [...], meta: {...} }`:
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
interface ApiResponse { data: User[]; meta: { total: number } }
|
|
1043
|
+
interface User { id: number; name: string }
|
|
1044
|
+
|
|
1045
|
+
const { data } = useApi<ApiResponse, User[]>('/users', {
|
|
1046
|
+
immediate: true,
|
|
1047
|
+
select: (res) => res.data,
|
|
1048
|
+
// data.value is User[], not ApiResponse
|
|
1049
|
+
})
|
|
947
1050
|
```
|
|
948
1051
|
|
|
1052
|
+
#### Transform Items
|
|
1053
|
+
|
|
1054
|
+
```typescript
|
|
1055
|
+
interface RawUser { id: number; firstName: string; lastName: string }
|
|
1056
|
+
interface User { id: number; fullName: string }
|
|
1057
|
+
|
|
1058
|
+
const { data } = useApi<RawUser[], User[]>('/users', {
|
|
1059
|
+
immediate: true,
|
|
1060
|
+
select: (users) => users.map(u => ({
|
|
1061
|
+
id: u.id,
|
|
1062
|
+
fullName: `${u.firstName} ${u.lastName}`,
|
|
1063
|
+
})),
|
|
1064
|
+
})
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
#### Filter Results
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
const { data } = useApi<Task[]>('/tasks', {
|
|
1071
|
+
immediate: true,
|
|
1072
|
+
select: (tasks) => tasks.filter(t => t.status === 'active'),
|
|
1073
|
+
})
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
#### select vs mutate
|
|
1077
|
+
|
|
1078
|
+
| | `select` | `mutate` |
|
|
1079
|
+
|---|---|---|
|
|
1080
|
+
| When it runs | On every successful response (auto) | When you call it manually |
|
|
1081
|
+
| With polling | Re-applied on every tick | Need to call in `onSuccess` each time |
|
|
1082
|
+
| With SWR | Re-applied on revalidation | Need to call in `onSuccess` |
|
|
1083
|
+
| `onSuccess` receives | Raw `AxiosResponse<TRaw>` | โ |
|
|
1084
|
+
|
|
1085
|
+
> [!NOTE]
|
|
1086
|
+
> `onSuccess` always receives the **raw** `AxiosResponse` from the server, not the selected value.
|
|
1087
|
+
> This lets you access headers, status, and the original shape if needed.
|
|
1088
|
+
|
|
1089
|
+
> [!NOTE]
|
|
1090
|
+
> The cache always stores the **raw** server response, not the selected value.
|
|
1091
|
+
> `select` is re-applied each time data is read from cache โ including SWR cache hits.
|
|
1092
|
+
> If you change your `select` function, the next cache hit will re-apply the new transformation.
|
|
1093
|
+
|
|
949
1094
|
---
|
|
950
1095
|
|
|
951
1096
|
## ๐ Real-World Examples
|
|
@@ -980,7 +1125,6 @@ const params = computed(() => ({
|
|
|
980
1125
|
|
|
981
1126
|
const { data, loading } = useApi<OrdersResponse>('/orders', {
|
|
982
1127
|
params,
|
|
983
|
-
watch: params,
|
|
984
1128
|
immediate: true
|
|
985
1129
|
})
|
|
986
1130
|
</script>
|
|
@@ -1277,6 +1421,8 @@ createApp(App).use(createApi({
|
|
|
1277
1421
|
| `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts for all requests |
|
|
1278
1422
|
| `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | Default HTTP status codes that trigger a retry across all requests |
|
|
1279
1423
|
| `useGlobalAbort` | `boolean` | `true` | When `true`, all requests subscribe to the global abort controller |
|
|
1424
|
+
| `refetchOnFocus` | `boolean \| { throttle?: number }` | `undefined` | Apply `refetchOnFocus` to all `useApi` instances. Per-request value takes precedence (including `false` to opt-out) |
|
|
1425
|
+
| `refetchOnReconnect` | `boolean` | `undefined` | Apply `refetchOnReconnect` to all `useApi` instances. Per-request value takes precedence (including `false` to opt-out) |
|
|
1280
1426
|
|
|
1281
1427
|
---
|
|
1282
1428
|
|
|
@@ -1352,6 +1498,38 @@ const api = createApiClient({
|
|
|
1352
1498
|
|
|
1353
1499
|
---
|
|
1354
1500
|
|
|
1501
|
+
### withCredentials โ Per-Request Cookie Control
|
|
1502
|
+
|
|
1503
|
+
**TL;DR: Override the Axios instance default for a single request without changing global settings.**
|
|
1504
|
+
|
|
1505
|
+
`withCredentials` controls whether cookies and other credentials are included in cross-origin requests (CORS). Set it globally in `createApiClient` and override it per request when needed.
|
|
1506
|
+
|
|
1507
|
+
```typescript
|
|
1508
|
+
// Global: withCredentials: false (Bearer token auth, no cookies)
|
|
1509
|
+
const api = createApiClient({ baseURL: '/api' })
|
|
1510
|
+
|
|
1511
|
+
// Override: this specific endpoint needs cookies
|
|
1512
|
+
const { data } = useApi('/user/session', {
|
|
1513
|
+
withCredentials: true,
|
|
1514
|
+
immediate: true,
|
|
1515
|
+
})
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
// Global: withCredentials: true (full cookie-based auth)
|
|
1520
|
+
const api = createApiClient({ baseURL: '/api', withCredentials: true })
|
|
1521
|
+
|
|
1522
|
+
// Override: skip cookies for a public CDN request
|
|
1523
|
+
const { data } = useApi('https://cdn.example.com/config.json', {
|
|
1524
|
+
withCredentials: false,
|
|
1525
|
+
immediate: true,
|
|
1526
|
+
})
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
Omitting `withCredentials` in `useApi` options means the Axios instance default is used โ no override applied.
|
|
1530
|
+
|
|
1531
|
+
---
|
|
1532
|
+
|
|
1355
1533
|
### Saving Tokens After Login
|
|
1356
1534
|
|
|
1357
1535
|
```typescript
|
|
@@ -1660,14 +1838,22 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1660
1838
|
|
|
1661
1839
|
## ๐ API Reference
|
|
1662
1840
|
|
|
1663
|
-
### `useApi<
|
|
1841
|
+
### `useApi<TRaw, D, TSelected>(url, options)`
|
|
1842
|
+
|
|
1843
|
+
Three type parameters โ all optional with defaults:
|
|
1844
|
+
|
|
1845
|
+
| Parameter | Default | Description |
|
|
1846
|
+
|-----------|---------|-------------|
|
|
1847
|
+
| `TRaw` | `unknown` | Shape of the raw response data from the server |
|
|
1848
|
+
| `D` | `unknown` | Shape of the request body / params |
|
|
1849
|
+
| `TSelected` | `TRaw` | Shape of `data.value` after `select` is applied. Equals `TRaw` when `select` is not used |
|
|
1664
1850
|
|
|
1665
1851
|
**Arguments:**
|
|
1666
1852
|
|
|
1667
1853
|
| Argument | Type | Description |
|
|
1668
1854
|
|----------|------|-------------|
|
|
1669
1855
|
| `url` | `MaybeRefOrGetter<string \| undefined>` | API endpoint. String, ref, or getter function. Returning `undefined` prevents the request. |
|
|
1670
|
-
| `options` | `UseApiOptions<
|
|
1856
|
+
| `options` | `UseApiOptions<TRaw, D, TSelected>` | Configuration object (see below). |
|
|
1671
1857
|
|
|
1672
1858
|
---
|
|
1673
1859
|
|
|
@@ -1688,16 +1874,23 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1688
1874
|
| Option | Type | Default | Description |
|
|
1689
1875
|
|--------|------|---------|-------------|
|
|
1690
1876
|
| `immediate` | `boolean` | `false` | When `true`, executes the request automatically when the composable is created |
|
|
1691
|
-
| `
|
|
1692
|
-
| `debounce` | `number` | `0` | Milliseconds to
|
|
1877
|
+
| `lazy` | `boolean` | `false` | Disable auto-tracking โ reactive changes to `url`, `params`, and `data` will NOT trigger a re-fetch |
|
|
1878
|
+
| `debounce` | `number` | `0` | Milliseconds to debounce auto-tracked re-fetches |
|
|
1693
1879
|
|
|
1694
1880
|
**Caching:**
|
|
1695
1881
|
|
|
1696
1882
|
| Option | Type | Default | Description |
|
|
1697
1883
|
|--------|------|---------|-------------|
|
|
1698
|
-
| `cache` | `string \| CacheOptions` | `undefined` | Cache the response in memory. String shorthand uses default 5-min TTL. See [Response Caching](#response-caching) |
|
|
1884
|
+
| `cache` | `string \| CacheOptions` | `undefined` | Cache the response in memory. String shorthand uses default 5-min TTL. `CacheOptions.swr: true` enables stale-while-revalidate. See [Response Caching](#response-caching) |
|
|
1699
1885
|
| `invalidateCache` | `string \| string[]` | `undefined` | Cache key(s) to delete on 2xx success. Never fires on error |
|
|
1700
1886
|
|
|
1887
|
+
**Refetch Triggers:**
|
|
1888
|
+
|
|
1889
|
+
| Option | Type | Default | Description |
|
|
1890
|
+
|--------|------|---------|-------------|
|
|
1891
|
+
| `refetchOnFocus` | `boolean \| { throttle?: number }` | `undefined` | Re-fetch when the browser tab regains focus. `true` uses a 60s throttle. Pass `{ throttle: 0 }` to always re-fetch. Configurable globally via `globalOptions` |
|
|
1892
|
+
| `refetchOnReconnect` | `boolean` | `undefined` | Re-fetch when the browser regains network connectivity (`online` event). No throttle. Configurable globally via `globalOptions` |
|
|
1893
|
+
|
|
1701
1894
|
**Polling:**
|
|
1702
1895
|
|
|
1703
1896
|
| Option | Type | Default | Description |
|
|
@@ -1712,11 +1905,17 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1712
1905
|
| `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts |
|
|
1713
1906
|
| `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | HTTP status codes that trigger a retry. `[]` means retry on any error |
|
|
1714
1907
|
|
|
1908
|
+
**Data Transformation:**
|
|
1909
|
+
|
|
1910
|
+
| Option | Type | Default | Description |
|
|
1911
|
+
|--------|------|---------|-------------|
|
|
1912
|
+
| `select` | `(data: TRaw) => TSelected` | `undefined` | Transform response data before it is stored in `data`. Re-applied on every fetch, polling tick, and SWR revalidation. Cache always stores raw data. See [select](#select--declarative-data-transformation) |
|
|
1913
|
+
|
|
1715
1914
|
**State Initialization:**
|
|
1716
1915
|
|
|
1717
1916
|
| Option | Type | Default | Description |
|
|
1718
1917
|
|--------|------|---------|-------------|
|
|
1719
|
-
| `initialData` | `
|
|
1918
|
+
| `initialData` | `TSelected` | `null` | Initial value for `data` before the first request completes |
|
|
1720
1919
|
| `initialLoading` | `boolean` | `false` | Initial value for `loading` โ set `true` to show a spinner before the first request fires |
|
|
1721
1920
|
|
|
1722
1921
|
**Lifecycle Hooks:**
|
|
@@ -1734,6 +1933,12 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1734
1933
|
|--------|------|---------|-------------|
|
|
1735
1934
|
| `skipErrorNotification` | `boolean` | `false` | When `true`, the global `onError` handler is NOT called for this request |
|
|
1736
1935
|
|
|
1936
|
+
**Credentials:**
|
|
1937
|
+
|
|
1938
|
+
| Option | Type | Default | Description |
|
|
1939
|
+
|--------|------|---------|-------------|
|
|
1940
|
+
| `withCredentials` | `boolean` | `undefined` | Override the Axios instance default for this request only. `true` = send cookies/credentials, `false` = omit them. Omitting uses the instance default set in `createApiClient` |
|
|
1941
|
+
|
|
1737
1942
|
**Advanced:**
|
|
1738
1943
|
|
|
1739
1944
|
| Option | Type | Default | Description |
|
|
@@ -1746,16 +1951,17 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1746
1951
|
|
|
1747
1952
|
| Name | Type | Description |
|
|
1748
1953
|
|------|------|-------------|
|
|
1749
|
-
| `data` | `Ref<
|
|
1954
|
+
| `data` | `Ref<TSelected \| null>` | Response data from the last successful request (transformed by `select` if provided) |
|
|
1750
1955
|
| `loading` | `Ref<boolean>` | `true` while a request is in flight (including retry delays) |
|
|
1751
1956
|
| `error` | `Ref<ApiError \| null>` | Error from the last failed request; `null` on success |
|
|
1752
1957
|
| `statusCode` | `Ref<number \| null>` | HTTP status code from the last completed request |
|
|
1753
|
-
| `response` | `Ref<AxiosResponse<
|
|
1754
|
-
| `
|
|
1755
|
-
| `
|
|
1958
|
+
| `response` | `Ref<AxiosResponse<unknown> \| null>` | Full Axios response object including headers (raw, before `select`) |
|
|
1959
|
+
| `revalidating` | `Ref<boolean>` | `true` while a background SWR revalidation is in flight. Always `false` when `cache: { swr: true }` is not set |
|
|
1960
|
+
| `execute(config?)` | `(config?: ApiRequestConfig<D>) => Promise<TSelected \| null>` | Manually trigger the request, optionally overriding options |
|
|
1961
|
+
| `mutate(newData)` | `(newData: TSelected \| null \| ((prev: TSelected \| null) => TSelected \| null)) => void` | Update `data` locally without a network request; clears `error` |
|
|
1756
1962
|
| `abort(msg?)` | `(message?: string) => void` | Cancel the current in-flight request |
|
|
1757
1963
|
| `reset()` | `() => void` | Cancel the request and reset all state to initial values |
|
|
1758
|
-
| `ignoreUpdates(fn)` | `(updater: () => void) => void` | Run `updater` without triggering
|
|
1964
|
+
| `ignoreUpdates(fn)` | `(updater: () => void) => void` | Run `updater` without triggering auto-tracked re-execution |
|
|
1759
1965
|
|
|
1760
1966
|
#### `execute(config?)`
|
|
1761
1967
|
|
|
@@ -1998,7 +2204,7 @@ function logout() {
|
|
|
1998
2204
|
|
|
1999
2205
|
### 1. Search with Debounce and Reset
|
|
2000
2206
|
|
|
2001
|
-
|
|
2207
|
+
Debounced search input. Typing triggers a request; "Clear" resets the input silently without fetching.
|
|
2002
2208
|
|
|
2003
2209
|
```vue
|
|
2004
2210
|
<script setup lang="ts">
|
|
@@ -2012,30 +2218,25 @@ interface User {
|
|
|
2012
2218
|
}
|
|
2013
2219
|
|
|
2014
2220
|
const search = ref('')
|
|
2015
|
-
const page = ref(1)
|
|
2016
2221
|
|
|
2017
|
-
const { data, loading,
|
|
2018
|
-
() =>
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
immediate: true
|
|
2023
|
-
}
|
|
2024
|
-
)
|
|
2222
|
+
const { data, loading, ignoreUpdates } = useApi<User[]>('/users', {
|
|
2223
|
+
params: () => ({ q: search.value }),
|
|
2224
|
+
debounce: 400,
|
|
2225
|
+
immediate: true,
|
|
2226
|
+
})
|
|
2025
2227
|
|
|
2026
|
-
function
|
|
2228
|
+
function clearSearch() {
|
|
2027
2229
|
ignoreUpdates(() => {
|
|
2028
2230
|
search.value = ''
|
|
2029
|
-
page.value = 1
|
|
2030
2231
|
})
|
|
2031
|
-
|
|
2232
|
+
// auto-tracking is paused โ no request fires on clear
|
|
2032
2233
|
}
|
|
2033
2234
|
</script>
|
|
2034
2235
|
|
|
2035
2236
|
<template>
|
|
2036
2237
|
<div>
|
|
2037
2238
|
<input v-model="search" placeholder="Search users..." />
|
|
2038
|
-
<button @click="
|
|
2239
|
+
<button @click="clearSearch">Clear</button>
|
|
2039
2240
|
|
|
2040
2241
|
<div v-if="loading">Searching...</div>
|
|
2041
2242
|
<ul v-else>
|
|
@@ -2051,7 +2252,7 @@ function resetSearch() {
|
|
|
2051
2252
|
|
|
2052
2253
|
### 2. Paginated List with Filter Reset
|
|
2053
2254
|
|
|
2054
|
-
When the user changes a filter, reset the page to 1
|
|
2255
|
+
When the user changes a filter, also reset the page to 1. Vue batches synchronous ref changes, so auto-tracking fires once โ no `ignoreUpdates` needed here.
|
|
2055
2256
|
|
|
2056
2257
|
```vue
|
|
2057
2258
|
<script setup lang="ts">
|
|
@@ -2067,18 +2268,15 @@ interface Post {
|
|
|
2067
2268
|
const page = ref(1)
|
|
2068
2269
|
const status = ref('all')
|
|
2069
2270
|
|
|
2070
|
-
const { data, loading
|
|
2071
|
-
() =>
|
|
2072
|
-
|
|
2073
|
-
)
|
|
2271
|
+
const { data, loading } = useApi<Post[]>('/posts', {
|
|
2272
|
+
params: () => ({ status: status.value, page: page.value }),
|
|
2273
|
+
immediate: true,
|
|
2274
|
+
})
|
|
2074
2275
|
|
|
2075
2276
|
function changeStatus(newStatus: string) {
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
page.value = 1
|
|
2080
|
-
})
|
|
2081
|
-
execute()
|
|
2277
|
+
status.value = newStatus
|
|
2278
|
+
page.value = 1
|
|
2279
|
+
// Vue batches these sync changes โ auto-tracking fires once, one request
|
|
2082
2280
|
}
|
|
2083
2281
|
</script>
|
|
2084
2282
|
|
|
@@ -2282,36 +2480,35 @@ Start polling every 2 seconds and stop automatically when the job reaches a term
|
|
|
2282
2480
|
|
|
2283
2481
|
```vue
|
|
2284
2482
|
<script setup lang="ts">
|
|
2285
|
-
import { ref } from 'vue'
|
|
2286
|
-
import { useApi } from '@ametie/vue-muza-use'
|
|
2483
|
+
import { ref } from 'vue'
|
|
2484
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
2287
2485
|
|
|
2288
|
-
interface JobStatus {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
}
|
|
2486
|
+
interface JobStatus {
|
|
2487
|
+
id: string
|
|
2488
|
+
status: 'pending' | 'processing' | 'complete' | 'failed'
|
|
2489
|
+
progress: number
|
|
2490
|
+
}
|
|
2293
2491
|
|
|
2294
|
-
const jobId = ref<string | null>(null)
|
|
2295
|
-
const pollInterval = ref(0)
|
|
2492
|
+
const jobId = ref<string | null>(null)
|
|
2493
|
+
const pollInterval = ref(0)
|
|
2494
|
+
|
|
2495
|
+
const { data: job, error } = useApi<JobStatus>(
|
|
2496
|
+
() => jobId.value ? `/jobs/${jobId.value}` : undefined,
|
|
2497
|
+
{
|
|
2498
|
+
poll: { interval: pollInterval },
|
|
2499
|
+
onSuccess(response) {
|
|
2500
|
+
const { status } = response.data
|
|
2501
|
+
if (status === 'complete' || status === 'failed') {
|
|
2502
|
+
pollInterval.value = 0 // Stop polling
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
)
|
|
2296
2507
|
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
watch: jobId,
|
|
2301
|
-
poll: { interval: pollInterval },
|
|
2302
|
-
onSuccess(response) {
|
|
2303
|
-
const { status } = response.data
|
|
2304
|
-
if (status === 'complete' || status === 'failed') {
|
|
2305
|
-
pollInterval.value = 0 // Stop polling
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2508
|
+
function startJob(id: string) {
|
|
2509
|
+
jobId.value = id
|
|
2510
|
+
pollInterval.value = 2000 // Start polling every 2s
|
|
2308
2511
|
}
|
|
2309
|
-
)
|
|
2310
|
-
|
|
2311
|
-
function startJob(id: string) {
|
|
2312
|
-
jobId.value = id
|
|
2313
|
-
pollInterval.value = 2000 // Start polling every 2s
|
|
2314
|
-
}
|
|
2315
2512
|
</script>
|
|
2316
2513
|
|
|
2317
2514
|
<template>
|
|
@@ -2332,7 +2529,7 @@ function startJob(id: string) {
|
|
|
2332
2529
|
| Problem | Likely Cause | Fix |
|
|
2333
2530
|
|---------|--------------|-----|
|
|
2334
2531
|
| `"createApi config not found"` | `createApi()` not called | Call `app.use(createApi(...))` in `main.ts` before mounting |
|
|
2335
|
-
| Request fires twice on mount | `immediate: true`
|
|
2532
|
+
| Request fires twice on mount | `immediate: true` fires on mount; auto-tracking also fires when a dep changes immediately | Ensure deps don't change synchronously right after mount, or use `lazy: true` with manual `execute()` |
|
|
2336
2533
|
| `retry` option does nothing | Default is `retry: false` | Set `retry: true` or `retry: 3` |
|
|
2337
2534
|
| ALL errors trigger retry, not just some | `retryStatusCodes` not set โ uses library default | Specify exact codes or use `retryStatusCodes: []` to retry on any error |
|
|
2338
2535
|
| `ignoreUpdates` still triggers a request | Updater function contains an `await` | `ignoreUpdates` is sync-only โ move async logic outside the updater |
|