@ametie/vue-muza-use 0.9.1 โ 0.10.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 +205 -7
- package/dist/index.cjs +29 -10
- package/dist/index.d.cts +100 -9
- package/dist/index.d.ts +100 -9
- package/dist/index.mjs +29 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
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-useApi/blob/main/.claude/skills/use-api/SKILL.md)**
|
|
19
|
+
|
|
12
20
|
---
|
|
13
21
|
|
|
14
22
|
## โจ Features
|
|
@@ -23,11 +31,14 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
23
31
|
- ๐งน **Zero Memory Leaks** โ Automatic cleanup of pending requests on component unmount
|
|
24
32
|
- ๐ **ignoreUpdates** โ Atomic updates without triggering intermediate requests
|
|
25
33
|
- ๐๏ธ **Response Caching** โ In-memory cache with configurable TTL and manual invalidation
|
|
34
|
+
- โก **Stale-While-Revalidate** โ Serve cached data instantly while refreshing silently in the background
|
|
35
|
+
- ๐ฌ **select** โ Transform or filter response data declaratively; re-applied on every fetch automatically
|
|
26
36
|
|
|
27
37
|
**Advanced Features** (When you need them):
|
|
28
38
|
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic with configurable status codes
|
|
29
39
|
- ๐ **JWT Token Management** โ Automatic token refresh with request queueing on 401 responses
|
|
30
40
|
- ๐๏ธ **Flexible Architecture** โ Bring your own Axios instance with full configuration control
|
|
41
|
+
- ๐ช **withCredentials** โ Per-request cookie and cross-origin credential control
|
|
31
42
|
|
|
32
43
|
---
|
|
33
44
|
|
|
@@ -42,11 +53,13 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
42
53
|
- [Watch & Auto-Refetch](#watch--auto-refetch)
|
|
43
54
|
- [ignoreUpdates โ Atomic Updates Without Refetch](#ignoreupdates--atomic-updates-without-refetch)
|
|
44
55
|
- [Response Caching](#response-caching)
|
|
56
|
+
- [Stale-While-Revalidate (SWR)](#stale-while-revalidate-swr)
|
|
45
57
|
- [Polling (Background Updates)](#polling-background-updates)
|
|
46
58
|
- [Error Handling](#error-handling)
|
|
47
59
|
- [retry โ Automatic Request Retry](#retry--automatic-request-retry)
|
|
48
60
|
- [Loading States](#loading-states)
|
|
49
61
|
- [Manual Data Updates (mutate)](#manual-data-updates-mutate)
|
|
62
|
+
- [select โ Declarative Data Transformation](#select--declarative-data-transformation)
|
|
50
63
|
|
|
51
64
|
**Real-World Examples:**
|
|
52
65
|
- [Data Table with Pagination](#data-table-with-pagination--sorting)
|
|
@@ -679,6 +692,70 @@ The following are intentionally **not** supported in v1:
|
|
|
679
692
|
|
|
680
693
|
---
|
|
681
694
|
|
|
695
|
+
### Stale-While-Revalidate (SWR)
|
|
696
|
+
|
|
697
|
+
**TL;DR: Return cached data instantly while fetching fresh data in the background. No loading spinner, no blank screen.**
|
|
698
|
+
|
|
699
|
+
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.
|
|
700
|
+
|
|
701
|
+
On a **cache miss** (first load), the request behaves exactly like a normal request โ `loading: true`, no stale data.
|
|
702
|
+
|
|
703
|
+
#### Basic Usage
|
|
704
|
+
|
|
705
|
+
```vue
|
|
706
|
+
<script setup lang="ts">
|
|
707
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
708
|
+
|
|
709
|
+
interface User { id: number; name: string }
|
|
710
|
+
|
|
711
|
+
const { data, revalidating } = useApi<User[]>('/users', {
|
|
712
|
+
cache: 'users',
|
|
713
|
+
staleWhileRevalidate: true,
|
|
714
|
+
immediate: true,
|
|
715
|
+
})
|
|
716
|
+
</script>
|
|
717
|
+
|
|
718
|
+
<template>
|
|
719
|
+
<!-- data renders immediately from cache โ no blank screen -->
|
|
720
|
+
<ul>
|
|
721
|
+
<li v-for="user in data" :key="user.id">
|
|
722
|
+
{{ user.name }}
|
|
723
|
+
<span v-if="revalidating">โป</span>
|
|
724
|
+
</li>
|
|
725
|
+
</ul>
|
|
726
|
+
</template>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### SWR vs Normal Cache Hit
|
|
730
|
+
|
|
731
|
+
| | Normal cache hit | SWR cache hit |
|
|
732
|
+
|---|---|---|
|
|
733
|
+
| `loading` | `false` | `false` |
|
|
734
|
+
| `data` | Stale data, no new request | Stale data โ then fresh data |
|
|
735
|
+
| `revalidating` | `false` | `true` while fetching, then `false` |
|
|
736
|
+
| Axios request | **Not made** | **Made** (silent background fetch) |
|
|
737
|
+
| `onBefore` | Not called | **Not called** (silent) |
|
|
738
|
+
| `onSuccess` | Not called | **Called** with fresh response |
|
|
739
|
+
| `onFinish` | Not called | **Called** after background fetch |
|
|
740
|
+
|
|
741
|
+
#### Error Handling
|
|
742
|
+
|
|
743
|
+
If the background revalidation request fails:
|
|
744
|
+
- `revalidating` resets to `false`
|
|
745
|
+
- `error` is set
|
|
746
|
+
- The **stale data is preserved** โ your UI doesn't go blank
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
const { data, revalidating, error } = useApi('/dashboard', {
|
|
750
|
+
cache: 'dashboard',
|
|
751
|
+
staleWhileRevalidate: true,
|
|
752
|
+
immediate: true,
|
|
753
|
+
})
|
|
754
|
+
// data.value stays the cached value even after a failed revalidation
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
682
759
|
### Polling (Background Updates)
|
|
683
760
|
|
|
684
761
|
**TL;DR: Keep data fresh with smart polling that automatically pauses when the browser tab is hidden.**
|
|
@@ -948,6 +1025,73 @@ const { mutate } = useApi<User[]>('/users', { immediate: true })
|
|
|
948
1025
|
|
|
949
1026
|
---
|
|
950
1027
|
|
|
1028
|
+
### select โ Declarative Data Transformation
|
|
1029
|
+
|
|
1030
|
+
**TL;DR: Transform, filter, or reshape response data once โ it's re-applied automatically on every fetch, polling tick, and SWR revalidation.**
|
|
1031
|
+
|
|
1032
|
+
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.
|
|
1033
|
+
|
|
1034
|
+
The second generic parameter of `useApi` controls the output type of `select`.
|
|
1035
|
+
|
|
1036
|
+
#### Extract a Nested Field
|
|
1037
|
+
|
|
1038
|
+
APIs that wrap responses in `{ data: [...], meta: {...} }`:
|
|
1039
|
+
|
|
1040
|
+
```typescript
|
|
1041
|
+
interface ApiResponse { data: User[]; meta: { total: number } }
|
|
1042
|
+
interface User { id: number; name: string }
|
|
1043
|
+
|
|
1044
|
+
const { data } = useApi<ApiResponse, User[]>('/users', {
|
|
1045
|
+
immediate: true,
|
|
1046
|
+
select: (res) => res.data,
|
|
1047
|
+
// data.value is User[], not ApiResponse
|
|
1048
|
+
})
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
#### Transform Items
|
|
1052
|
+
|
|
1053
|
+
```typescript
|
|
1054
|
+
interface RawUser { id: number; firstName: string; lastName: string }
|
|
1055
|
+
interface User { id: number; fullName: string }
|
|
1056
|
+
|
|
1057
|
+
const { data } = useApi<RawUser[], User[]>('/users', {
|
|
1058
|
+
immediate: true,
|
|
1059
|
+
select: (users) => users.map(u => ({
|
|
1060
|
+
id: u.id,
|
|
1061
|
+
fullName: `${u.firstName} ${u.lastName}`,
|
|
1062
|
+
})),
|
|
1063
|
+
})
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
#### Filter Results
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
const { data } = useApi<Task[]>('/tasks', {
|
|
1070
|
+
immediate: true,
|
|
1071
|
+
select: (tasks) => tasks.filter(t => t.status === 'active'),
|
|
1072
|
+
})
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
#### select vs mutate
|
|
1076
|
+
|
|
1077
|
+
| | `select` | `mutate` |
|
|
1078
|
+
|---|---|---|
|
|
1079
|
+
| When it runs | On every successful response (auto) | When you call it manually |
|
|
1080
|
+
| With polling | Re-applied on every tick | Need to call in `onSuccess` each time |
|
|
1081
|
+
| With SWR | Re-applied on revalidation | Need to call in `onSuccess` |
|
|
1082
|
+
| `onSuccess` receives | Raw `AxiosResponse<TRaw>` | โ |
|
|
1083
|
+
|
|
1084
|
+
> [!NOTE]
|
|
1085
|
+
> `onSuccess` always receives the **raw** `AxiosResponse` from the server, not the selected value.
|
|
1086
|
+
> This lets you access headers, status, and the original shape if needed.
|
|
1087
|
+
|
|
1088
|
+
> [!NOTE]
|
|
1089
|
+
> The cache always stores the **raw** server response, not the selected value.
|
|
1090
|
+
> `select` is re-applied each time data is read from cache โ including SWR cache hits.
|
|
1091
|
+
> If you change your `select` function, the next cache hit will re-apply the new transformation.
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
951
1095
|
## ๐ Real-World Examples
|
|
952
1096
|
|
|
953
1097
|
### Data Table with Pagination & Sorting
|
|
@@ -1352,6 +1496,38 @@ const api = createApiClient({
|
|
|
1352
1496
|
|
|
1353
1497
|
---
|
|
1354
1498
|
|
|
1499
|
+
### withCredentials โ Per-Request Cookie Control
|
|
1500
|
+
|
|
1501
|
+
**TL;DR: Override the Axios instance default for a single request without changing global settings.**
|
|
1502
|
+
|
|
1503
|
+
`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.
|
|
1504
|
+
|
|
1505
|
+
```typescript
|
|
1506
|
+
// Global: withCredentials: false (Bearer token auth, no cookies)
|
|
1507
|
+
const api = createApiClient({ baseURL: '/api' })
|
|
1508
|
+
|
|
1509
|
+
// Override: this specific endpoint needs cookies
|
|
1510
|
+
const { data } = useApi('/user/session', {
|
|
1511
|
+
withCredentials: true,
|
|
1512
|
+
immediate: true,
|
|
1513
|
+
})
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
```typescript
|
|
1517
|
+
// Global: withCredentials: true (full cookie-based auth)
|
|
1518
|
+
const api = createApiClient({ baseURL: '/api', withCredentials: true })
|
|
1519
|
+
|
|
1520
|
+
// Override: skip cookies for a public CDN request
|
|
1521
|
+
const { data } = useApi('https://cdn.example.com/config.json', {
|
|
1522
|
+
withCredentials: false,
|
|
1523
|
+
immediate: true,
|
|
1524
|
+
})
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
Omitting `withCredentials` in `useApi` options means the Axios instance default is used โ no override applied.
|
|
1528
|
+
|
|
1529
|
+
---
|
|
1530
|
+
|
|
1355
1531
|
### Saving Tokens After Login
|
|
1356
1532
|
|
|
1357
1533
|
```typescript
|
|
@@ -1660,14 +1836,22 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1660
1836
|
|
|
1661
1837
|
## ๐ API Reference
|
|
1662
1838
|
|
|
1663
|
-
### `useApi<
|
|
1839
|
+
### `useApi<TRaw, D, TSelected>(url, options)`
|
|
1840
|
+
|
|
1841
|
+
Three type parameters โ all optional with defaults:
|
|
1842
|
+
|
|
1843
|
+
| Parameter | Default | Description |
|
|
1844
|
+
|-----------|---------|-------------|
|
|
1845
|
+
| `TRaw` | `unknown` | Shape of the raw response data from the server |
|
|
1846
|
+
| `D` | `unknown` | Shape of the request body / params |
|
|
1847
|
+
| `TSelected` | `TRaw` | Shape of `data.value` after `select` is applied. Equals `TRaw` when `select` is not used |
|
|
1664
1848
|
|
|
1665
1849
|
**Arguments:**
|
|
1666
1850
|
|
|
1667
1851
|
| Argument | Type | Description |
|
|
1668
1852
|
|----------|------|-------------|
|
|
1669
1853
|
| `url` | `MaybeRefOrGetter<string \| undefined>` | API endpoint. String, ref, or getter function. Returning `undefined` prevents the request. |
|
|
1670
|
-
| `options` | `UseApiOptions<
|
|
1854
|
+
| `options` | `UseApiOptions<TRaw, D, TSelected>` | Configuration object (see below). |
|
|
1671
1855
|
|
|
1672
1856
|
---
|
|
1673
1857
|
|
|
@@ -1697,6 +1881,7 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1697
1881
|
|--------|------|---------|-------------|
|
|
1698
1882
|
| `cache` | `string \| CacheOptions` | `undefined` | Cache the response in memory. String shorthand uses default 5-min TTL. See [Response Caching](#response-caching) |
|
|
1699
1883
|
| `invalidateCache` | `string \| string[]` | `undefined` | Cache key(s) to delete on 2xx success. Never fires on error |
|
|
1884
|
+
| `staleWhileRevalidate` | `boolean` | `false` | When `true` and a cache hit occurs, return stale data immediately and revalidate in the background. `revalidating` is `true` during the background fetch. See [SWR](#stale-while-revalidate-swr) |
|
|
1700
1885
|
|
|
1701
1886
|
**Polling:**
|
|
1702
1887
|
|
|
@@ -1712,11 +1897,17 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1712
1897
|
| `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts |
|
|
1713
1898
|
| `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | HTTP status codes that trigger a retry. `[]` means retry on any error |
|
|
1714
1899
|
|
|
1900
|
+
**Data Transformation:**
|
|
1901
|
+
|
|
1902
|
+
| Option | Type | Default | Description |
|
|
1903
|
+
|--------|------|---------|-------------|
|
|
1904
|
+
| `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) |
|
|
1905
|
+
|
|
1715
1906
|
**State Initialization:**
|
|
1716
1907
|
|
|
1717
1908
|
| Option | Type | Default | Description |
|
|
1718
1909
|
|--------|------|---------|-------------|
|
|
1719
|
-
| `initialData` | `
|
|
1910
|
+
| `initialData` | `TSelected` | `null` | Initial value for `data` before the first request completes |
|
|
1720
1911
|
| `initialLoading` | `boolean` | `false` | Initial value for `loading` โ set `true` to show a spinner before the first request fires |
|
|
1721
1912
|
|
|
1722
1913
|
**Lifecycle Hooks:**
|
|
@@ -1734,6 +1925,12 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1734
1925
|
|--------|------|---------|-------------|
|
|
1735
1926
|
| `skipErrorNotification` | `boolean` | `false` | When `true`, the global `onError` handler is NOT called for this request |
|
|
1736
1927
|
|
|
1928
|
+
**Credentials:**
|
|
1929
|
+
|
|
1930
|
+
| Option | Type | Default | Description |
|
|
1931
|
+
|--------|------|---------|-------------|
|
|
1932
|
+
| `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` |
|
|
1933
|
+
|
|
1737
1934
|
**Advanced:**
|
|
1738
1935
|
|
|
1739
1936
|
| Option | Type | Default | Description |
|
|
@@ -1746,13 +1943,14 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
|
1746
1943
|
|
|
1747
1944
|
| Name | Type | Description |
|
|
1748
1945
|
|------|------|-------------|
|
|
1749
|
-
| `data` | `Ref<
|
|
1946
|
+
| `data` | `Ref<TSelected \| null>` | Response data from the last successful request (transformed by `select` if provided) |
|
|
1750
1947
|
| `loading` | `Ref<boolean>` | `true` while a request is in flight (including retry delays) |
|
|
1751
1948
|
| `error` | `Ref<ApiError \| null>` | Error from the last failed request; `null` on success |
|
|
1752
1949
|
| `statusCode` | `Ref<number \| null>` | HTTP status code from the last completed request |
|
|
1753
|
-
| `response` | `Ref<AxiosResponse<
|
|
1754
|
-
| `
|
|
1755
|
-
| `
|
|
1950
|
+
| `response` | `Ref<AxiosResponse<unknown> \| null>` | Full Axios response object including headers (raw, before `select`) |
|
|
1951
|
+
| `revalidating` | `Ref<boolean>` | `true` while a background SWR revalidation is in flight. Always `false` when `staleWhileRevalidate` is not set |
|
|
1952
|
+
| `execute(config?)` | `(config?: ApiRequestConfig<D>) => Promise<TSelected \| null>` | Manually trigger the request, optionally overriding options |
|
|
1953
|
+
| `mutate(newData)` | `(newData: TSelected \| null \| ((prev: TSelected \| null) => TSelected \| null)) => void` | Update `data` locally without a network request; clears `error` |
|
|
1756
1954
|
| `abort(msg?)` | `(message?: string) => void` | Cancel the current in-flight request |
|
|
1757
1955
|
| `reset()` | `() => void` | Cancel the request and reset all state to initial values |
|
|
1758
1956
|
| `ignoreUpdates(fn)` | `(updater: () => void) => void` | Run `updater` without triggering watch-based re-execution |
|
package/dist/index.cjs
CHANGED
|
@@ -285,11 +285,20 @@ function useApi(url, options = {}) {
|
|
|
285
285
|
useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
|
|
286
286
|
initialLoading = false,
|
|
287
287
|
poll = 0,
|
|
288
|
+
// Explicitly excluded from axiosConfig โ these are useApi-only options
|
|
289
|
+
// and must not be forwarded to axios.request()
|
|
290
|
+
cache: _cache,
|
|
291
|
+
invalidateCache: _invalidateCache,
|
|
292
|
+
watch: _watch,
|
|
293
|
+
staleWhileRevalidate = false,
|
|
294
|
+
select,
|
|
288
295
|
...axiosConfig
|
|
289
296
|
} = options;
|
|
290
297
|
const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
|
|
298
|
+
const applySelect = (raw) => select ? select(raw) : raw;
|
|
291
299
|
const startLoading = initialLoading ?? immediate;
|
|
292
300
|
const state = useApiState(initialData, { initialLoading: startLoading });
|
|
301
|
+
const revalidating = (0, import_vue4.ref)(false);
|
|
293
302
|
const abortController2 = (0, import_vue4.ref)(null);
|
|
294
303
|
const globalAbort = useGlobalAbort ? useAbortController() : null;
|
|
295
304
|
let pollTimer = null;
|
|
@@ -306,11 +315,16 @@ function useApi(url, options = {}) {
|
|
|
306
315
|
};
|
|
307
316
|
const executeRequest = async (config) => {
|
|
308
317
|
const cacheOpts = normalizeCacheOptions(options.cache);
|
|
318
|
+
let isRevalidating = false;
|
|
309
319
|
if (cacheOpts) {
|
|
310
320
|
const cached = readCache(cacheOpts.id);
|
|
311
321
|
if (cached !== null) {
|
|
312
|
-
state.mutate(cached);
|
|
313
|
-
|
|
322
|
+
state.mutate(applySelect(cached));
|
|
323
|
+
if (!staleWhileRevalidate) {
|
|
324
|
+
return applySelect(cached);
|
|
325
|
+
}
|
|
326
|
+
isRevalidating = true;
|
|
327
|
+
revalidating.value = true;
|
|
314
328
|
}
|
|
315
329
|
}
|
|
316
330
|
if (pollTimer) clearTimeout(pollTimer);
|
|
@@ -324,15 +338,16 @@ function useApi(url, options = {}) {
|
|
|
324
338
|
const gs = globalAbort.getSignal();
|
|
325
339
|
if (!gs.aborted) {
|
|
326
340
|
subscribedSignal = gs;
|
|
327
|
-
const currentCount = globalAbort.abortCount.value;
|
|
328
341
|
globalAbortHandler = () => {
|
|
329
|
-
|
|
342
|
+
controller.abort("Cancelled by global abort");
|
|
330
343
|
};
|
|
331
344
|
gs.addEventListener("abort", globalAbortHandler);
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
if (!isRevalidating) {
|
|
348
|
+
onBefore?.();
|
|
349
|
+
state.setLoading(true);
|
|
350
|
+
}
|
|
336
351
|
state.setError(null);
|
|
337
352
|
let wasCancelled = false;
|
|
338
353
|
let retryCount = 0;
|
|
@@ -356,7 +371,8 @@ function useApi(url, options = {}) {
|
|
|
356
371
|
signal: controller.signal,
|
|
357
372
|
...{ authMode: config?.authMode || authMode }
|
|
358
373
|
});
|
|
359
|
-
|
|
374
|
+
const selected = applySelect(response.data);
|
|
375
|
+
state.mutate(selected, response);
|
|
360
376
|
state.setStatusCode(response.status);
|
|
361
377
|
if (cacheOpts) {
|
|
362
378
|
writeCache(cacheOpts.id, response.data, cacheOpts.staleTime);
|
|
@@ -365,7 +381,7 @@ function useApi(url, options = {}) {
|
|
|
365
381
|
invalidateCache(options.invalidateCache);
|
|
366
382
|
}
|
|
367
383
|
onSuccess?.(response);
|
|
368
|
-
return
|
|
384
|
+
return selected;
|
|
369
385
|
} catch (err) {
|
|
370
386
|
if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
|
|
371
387
|
wasCancelled = true;
|
|
@@ -407,8 +423,9 @@ function useApi(url, options = {}) {
|
|
|
407
423
|
return null;
|
|
408
424
|
} finally {
|
|
409
425
|
if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
|
|
426
|
+
revalidating.value = false;
|
|
410
427
|
if (!wasCancelled) {
|
|
411
|
-
state.setLoading(false);
|
|
428
|
+
if (!isRevalidating) state.setLoading(false);
|
|
412
429
|
onFinish?.();
|
|
413
430
|
const { interval, whenHidden } = getPollConfig();
|
|
414
431
|
if (interval > 0) {
|
|
@@ -492,8 +509,10 @@ function useApi(url, options = {}) {
|
|
|
492
509
|
}
|
|
493
510
|
}, { deep: true });
|
|
494
511
|
}
|
|
495
|
-
return { ...state, execute, abort, reset, ignoreUpdates };
|
|
512
|
+
return { ...state, revalidating, execute, abort, reset, ignoreUpdates };
|
|
496
513
|
}
|
|
514
|
+
|
|
515
|
+
// src/useApi.helpers.ts
|
|
497
516
|
function useApiGet(url, options) {
|
|
498
517
|
return useApi(url, { ...options, method: "GET" });
|
|
499
518
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -37,18 +37,97 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
|
|
|
37
37
|
* Empty array = retry on any error (network errors included).
|
|
38
38
|
*/
|
|
39
39
|
retryStatusCodes?: number[];
|
|
40
|
+
/**
|
|
41
|
+
* Include credentials (cookies, Authorization headers) in cross-origin requests.
|
|
42
|
+
*
|
|
43
|
+
* Supports three auth strategies:
|
|
44
|
+
*
|
|
45
|
+
* **1. Bearer token (default)** โ tokens in localStorage, no cookies needed:
|
|
46
|
+
* ```ts
|
|
47
|
+
* createApiClient({ baseURL: '/api' }) // withCredentials: false by default
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* **2. Full cookie-based auth** โ server sets httpOnly cookies for both tokens:
|
|
51
|
+
* ```ts
|
|
52
|
+
* createApiClient({ baseURL: '/api', withCredentials: true })
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* **3. Hybrid** โ Bearer access token + httpOnly refresh cookie:
|
|
56
|
+
* ```ts
|
|
57
|
+
* createApiClient({
|
|
58
|
+
* baseURL: '/api',
|
|
59
|
+
* authOptions: { refreshWithCredentials: true } // cookies only on /auth/refresh
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* **Per-request override** โ override the global setting for a specific request:
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Global: withCredentials: false, but this endpoint needs cookies
|
|
66
|
+
* const { data } = useApi('/user/profile', { withCredentials: true })
|
|
67
|
+
*
|
|
68
|
+
* // Global: withCredentials: true, but skip cookies for public CDN
|
|
69
|
+
* const { data } = useApi('https://cdn.example.com/config.json', {
|
|
70
|
+
* withCredentials: false,
|
|
71
|
+
* immediate: true
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
withCredentials?: boolean;
|
|
40
76
|
}
|
|
41
|
-
interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
|
|
77
|
+
interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequestConfig<D> {
|
|
42
78
|
immediate?: boolean;
|
|
43
79
|
onSuccess?: (response: AxiosResponse<T>) => void;
|
|
44
80
|
onError?: (error: ApiError) => void;
|
|
45
81
|
onBefore?: () => void;
|
|
46
82
|
onFinish?: () => void;
|
|
47
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Transform the raw response data before it is stored in `data`.
|
|
85
|
+
* Applied on every successful response โ including polling, SWR revalidation,
|
|
86
|
+
* and watch-triggered re-fetches. The cache always stores the raw server data;
|
|
87
|
+
* `select` is re-applied each time data is read from cache.
|
|
88
|
+
*
|
|
89
|
+
* The second generic parameter of `useApi` becomes the output type of `select`.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* // Extract nested field
|
|
94
|
+
* const { data } = useApi<ApiResponse, User[]>('/users', {
|
|
95
|
+
* select: (res) => res.data,
|
|
96
|
+
* })
|
|
97
|
+
*
|
|
98
|
+
* // Transform items
|
|
99
|
+
* const { data } = useApi<RawUser[], User[]>('/users', {
|
|
100
|
+
* select: (users) => users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` })),
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
select?: (data: T) => TSelected;
|
|
105
|
+
initialData?: TSelected;
|
|
48
106
|
debounce?: number;
|
|
49
107
|
useGlobalAbort?: boolean;
|
|
50
108
|
initialLoading?: boolean;
|
|
51
109
|
watch?: WatchSource | WatchSource[];
|
|
110
|
+
/**
|
|
111
|
+
* Return cached data immediately and revalidate in the background.
|
|
112
|
+
*
|
|
113
|
+
* Requires the `cache` option to be set. On a cache hit the cached value
|
|
114
|
+
* is returned right away (no loading state, no spinner) while a fresh
|
|
115
|
+
* request runs silently. The `revalidating` ref is `true` during the
|
|
116
|
+
* background fetch so you can show a subtle indicator if needed.
|
|
117
|
+
*
|
|
118
|
+
* On a cache miss the request behaves normally (loading: true).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const { data, revalidating } = useApi('/users', {
|
|
123
|
+
* cache: 'users',
|
|
124
|
+
* staleWhileRevalidate: true,
|
|
125
|
+
* immediate: true,
|
|
126
|
+
* })
|
|
127
|
+
* // Template: <span v-if="revalidating">โป</span>
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
staleWhileRevalidate?: boolean;
|
|
52
131
|
/**
|
|
53
132
|
* Polling configuration.
|
|
54
133
|
* - Pass a **number** (ms) for simple polling.
|
|
@@ -81,7 +160,13 @@ interface UseApiReturn<T = unknown, D = unknown> {
|
|
|
81
160
|
loading: Ref<boolean>;
|
|
82
161
|
error: Ref<ApiError | null>;
|
|
83
162
|
statusCode: Ref<number | null>;
|
|
84
|
-
response: Ref<AxiosResponse<
|
|
163
|
+
response: Ref<AxiosResponse<unknown> | null>;
|
|
164
|
+
/**
|
|
165
|
+
* `true` while a background revalidation request is in-flight.
|
|
166
|
+
* Only active when `staleWhileRevalidate: true` and a cache hit occurred.
|
|
167
|
+
* Use it to show a subtle refresh indicator without blocking the UI.
|
|
168
|
+
*/
|
|
169
|
+
revalidating: Ref<boolean>;
|
|
85
170
|
execute: (config?: ApiRequestConfig<D>) => Promise<T | null | undefined>;
|
|
86
171
|
abort: (message?: string) => void;
|
|
87
172
|
reset: () => void;
|
|
@@ -254,7 +339,8 @@ declare function createApi(options: ApiPluginOptions): {
|
|
|
254
339
|
};
|
|
255
340
|
declare function useApiConfig(): ApiPluginOptions;
|
|
256
341
|
|
|
257
|
-
declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D>): UseApiReturn<
|
|
342
|
+
declare function useApi<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D, TSelected>): UseApiReturn<TSelected, D>;
|
|
343
|
+
|
|
258
344
|
/**
|
|
259
345
|
* Helper for GET requests
|
|
260
346
|
*
|
|
@@ -263,9 +349,14 @@ declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string |
|
|
|
263
349
|
* const { data, loading, error } = useApiGet<User[]>('/users', {
|
|
264
350
|
* immediate: true
|
|
265
351
|
* })
|
|
352
|
+
*
|
|
353
|
+
* // With select:
|
|
354
|
+
* const { data } = useApiGet<ApiResponse, User[]>('/users', {
|
|
355
|
+
* select: (res) => res.data,
|
|
356
|
+
* })
|
|
266
357
|
* ```
|
|
267
358
|
*/
|
|
268
|
-
declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<
|
|
359
|
+
declare function useApiGet<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
|
|
269
360
|
/**
|
|
270
361
|
* Helper for POST requests
|
|
271
362
|
*
|
|
@@ -275,7 +366,7 @@ declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined
|
|
|
275
366
|
* await execute({ data: { name: 'John' } })
|
|
276
367
|
* ```
|
|
277
368
|
*/
|
|
278
|
-
declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
369
|
+
declare function useApiPost<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
279
370
|
/**
|
|
280
371
|
* Helper for PUT requests
|
|
281
372
|
*
|
|
@@ -285,7 +376,7 @@ declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<stri
|
|
|
285
376
|
* await execute({ data: { name: 'John Doe' } })
|
|
286
377
|
* ```
|
|
287
378
|
*/
|
|
288
|
-
declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
379
|
+
declare function useApiPut<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
289
380
|
/**
|
|
290
381
|
* Helper for PATCH requests
|
|
291
382
|
*
|
|
@@ -295,7 +386,7 @@ declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<strin
|
|
|
295
386
|
* await execute({ data: { name: 'John' } })
|
|
296
387
|
* ```
|
|
297
388
|
*/
|
|
298
|
-
declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
389
|
+
declare function useApiPatch<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
299
390
|
/**
|
|
300
391
|
* Helper for DELETE requests
|
|
301
392
|
*
|
|
@@ -305,7 +396,7 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
|
|
|
305
396
|
* await execute()
|
|
306
397
|
* ```
|
|
307
398
|
*/
|
|
308
|
-
declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<
|
|
399
|
+
declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
|
|
309
400
|
|
|
310
401
|
/**
|
|
311
402
|
* Execute multiple API requests in parallel with full reactive state
|
package/dist/index.d.ts
CHANGED
|
@@ -37,18 +37,97 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
|
|
|
37
37
|
* Empty array = retry on any error (network errors included).
|
|
38
38
|
*/
|
|
39
39
|
retryStatusCodes?: number[];
|
|
40
|
+
/**
|
|
41
|
+
* Include credentials (cookies, Authorization headers) in cross-origin requests.
|
|
42
|
+
*
|
|
43
|
+
* Supports three auth strategies:
|
|
44
|
+
*
|
|
45
|
+
* **1. Bearer token (default)** โ tokens in localStorage, no cookies needed:
|
|
46
|
+
* ```ts
|
|
47
|
+
* createApiClient({ baseURL: '/api' }) // withCredentials: false by default
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* **2. Full cookie-based auth** โ server sets httpOnly cookies for both tokens:
|
|
51
|
+
* ```ts
|
|
52
|
+
* createApiClient({ baseURL: '/api', withCredentials: true })
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* **3. Hybrid** โ Bearer access token + httpOnly refresh cookie:
|
|
56
|
+
* ```ts
|
|
57
|
+
* createApiClient({
|
|
58
|
+
* baseURL: '/api',
|
|
59
|
+
* authOptions: { refreshWithCredentials: true } // cookies only on /auth/refresh
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* **Per-request override** โ override the global setting for a specific request:
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Global: withCredentials: false, but this endpoint needs cookies
|
|
66
|
+
* const { data } = useApi('/user/profile', { withCredentials: true })
|
|
67
|
+
*
|
|
68
|
+
* // Global: withCredentials: true, but skip cookies for public CDN
|
|
69
|
+
* const { data } = useApi('https://cdn.example.com/config.json', {
|
|
70
|
+
* withCredentials: false,
|
|
71
|
+
* immediate: true
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
withCredentials?: boolean;
|
|
40
76
|
}
|
|
41
|
-
interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
|
|
77
|
+
interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequestConfig<D> {
|
|
42
78
|
immediate?: boolean;
|
|
43
79
|
onSuccess?: (response: AxiosResponse<T>) => void;
|
|
44
80
|
onError?: (error: ApiError) => void;
|
|
45
81
|
onBefore?: () => void;
|
|
46
82
|
onFinish?: () => void;
|
|
47
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Transform the raw response data before it is stored in `data`.
|
|
85
|
+
* Applied on every successful response โ including polling, SWR revalidation,
|
|
86
|
+
* and watch-triggered re-fetches. The cache always stores the raw server data;
|
|
87
|
+
* `select` is re-applied each time data is read from cache.
|
|
88
|
+
*
|
|
89
|
+
* The second generic parameter of `useApi` becomes the output type of `select`.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* // Extract nested field
|
|
94
|
+
* const { data } = useApi<ApiResponse, User[]>('/users', {
|
|
95
|
+
* select: (res) => res.data,
|
|
96
|
+
* })
|
|
97
|
+
*
|
|
98
|
+
* // Transform items
|
|
99
|
+
* const { data } = useApi<RawUser[], User[]>('/users', {
|
|
100
|
+
* select: (users) => users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` })),
|
|
101
|
+
* })
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
select?: (data: T) => TSelected;
|
|
105
|
+
initialData?: TSelected;
|
|
48
106
|
debounce?: number;
|
|
49
107
|
useGlobalAbort?: boolean;
|
|
50
108
|
initialLoading?: boolean;
|
|
51
109
|
watch?: WatchSource | WatchSource[];
|
|
110
|
+
/**
|
|
111
|
+
* Return cached data immediately and revalidate in the background.
|
|
112
|
+
*
|
|
113
|
+
* Requires the `cache` option to be set. On a cache hit the cached value
|
|
114
|
+
* is returned right away (no loading state, no spinner) while a fresh
|
|
115
|
+
* request runs silently. The `revalidating` ref is `true` during the
|
|
116
|
+
* background fetch so you can show a subtle indicator if needed.
|
|
117
|
+
*
|
|
118
|
+
* On a cache miss the request behaves normally (loading: true).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const { data, revalidating } = useApi('/users', {
|
|
123
|
+
* cache: 'users',
|
|
124
|
+
* staleWhileRevalidate: true,
|
|
125
|
+
* immediate: true,
|
|
126
|
+
* })
|
|
127
|
+
* // Template: <span v-if="revalidating">โป</span>
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
staleWhileRevalidate?: boolean;
|
|
52
131
|
/**
|
|
53
132
|
* Polling configuration.
|
|
54
133
|
* - Pass a **number** (ms) for simple polling.
|
|
@@ -81,7 +160,13 @@ interface UseApiReturn<T = unknown, D = unknown> {
|
|
|
81
160
|
loading: Ref<boolean>;
|
|
82
161
|
error: Ref<ApiError | null>;
|
|
83
162
|
statusCode: Ref<number | null>;
|
|
84
|
-
response: Ref<AxiosResponse<
|
|
163
|
+
response: Ref<AxiosResponse<unknown> | null>;
|
|
164
|
+
/**
|
|
165
|
+
* `true` while a background revalidation request is in-flight.
|
|
166
|
+
* Only active when `staleWhileRevalidate: true` and a cache hit occurred.
|
|
167
|
+
* Use it to show a subtle refresh indicator without blocking the UI.
|
|
168
|
+
*/
|
|
169
|
+
revalidating: Ref<boolean>;
|
|
85
170
|
execute: (config?: ApiRequestConfig<D>) => Promise<T | null | undefined>;
|
|
86
171
|
abort: (message?: string) => void;
|
|
87
172
|
reset: () => void;
|
|
@@ -254,7 +339,8 @@ declare function createApi(options: ApiPluginOptions): {
|
|
|
254
339
|
};
|
|
255
340
|
declare function useApiConfig(): ApiPluginOptions;
|
|
256
341
|
|
|
257
|
-
declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D>): UseApiReturn<
|
|
342
|
+
declare function useApi<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D, TSelected>): UseApiReturn<TSelected, D>;
|
|
343
|
+
|
|
258
344
|
/**
|
|
259
345
|
* Helper for GET requests
|
|
260
346
|
*
|
|
@@ -263,9 +349,14 @@ declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string |
|
|
|
263
349
|
* const { data, loading, error } = useApiGet<User[]>('/users', {
|
|
264
350
|
* immediate: true
|
|
265
351
|
* })
|
|
352
|
+
*
|
|
353
|
+
* // With select:
|
|
354
|
+
* const { data } = useApiGet<ApiResponse, User[]>('/users', {
|
|
355
|
+
* select: (res) => res.data,
|
|
356
|
+
* })
|
|
266
357
|
* ```
|
|
267
358
|
*/
|
|
268
|
-
declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<
|
|
359
|
+
declare function useApiGet<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
|
|
269
360
|
/**
|
|
270
361
|
* Helper for POST requests
|
|
271
362
|
*
|
|
@@ -275,7 +366,7 @@ declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined
|
|
|
275
366
|
* await execute({ data: { name: 'John' } })
|
|
276
367
|
* ```
|
|
277
368
|
*/
|
|
278
|
-
declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
369
|
+
declare function useApiPost<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
279
370
|
/**
|
|
280
371
|
* Helper for PUT requests
|
|
281
372
|
*
|
|
@@ -285,7 +376,7 @@ declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<stri
|
|
|
285
376
|
* await execute({ data: { name: 'John Doe' } })
|
|
286
377
|
* ```
|
|
287
378
|
*/
|
|
288
|
-
declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
379
|
+
declare function useApiPut<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
289
380
|
/**
|
|
290
381
|
* Helper for PATCH requests
|
|
291
382
|
*
|
|
@@ -295,7 +386,7 @@ declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<strin
|
|
|
295
386
|
* await execute({ data: { name: 'John' } })
|
|
296
387
|
* ```
|
|
297
388
|
*/
|
|
298
|
-
declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<
|
|
389
|
+
declare function useApiPatch<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
|
|
299
390
|
/**
|
|
300
391
|
* Helper for DELETE requests
|
|
301
392
|
*
|
|
@@ -305,7 +396,7 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
|
|
|
305
396
|
* await execute()
|
|
306
397
|
* ```
|
|
307
398
|
*/
|
|
308
|
-
declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<
|
|
399
|
+
declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
|
|
309
400
|
|
|
310
401
|
/**
|
|
311
402
|
* Execute multiple API requests in parallel with full reactive state
|
package/dist/index.mjs
CHANGED
|
@@ -232,11 +232,20 @@ function useApi(url, options = {}) {
|
|
|
232
232
|
useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
|
|
233
233
|
initialLoading = false,
|
|
234
234
|
poll = 0,
|
|
235
|
+
// Explicitly excluded from axiosConfig โ these are useApi-only options
|
|
236
|
+
// and must not be forwarded to axios.request()
|
|
237
|
+
cache: _cache,
|
|
238
|
+
invalidateCache: _invalidateCache,
|
|
239
|
+
watch: _watch,
|
|
240
|
+
staleWhileRevalidate = false,
|
|
241
|
+
select,
|
|
235
242
|
...axiosConfig
|
|
236
243
|
} = options;
|
|
237
244
|
const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
|
|
245
|
+
const applySelect = (raw) => select ? select(raw) : raw;
|
|
238
246
|
const startLoading = initialLoading ?? immediate;
|
|
239
247
|
const state = useApiState(initialData, { initialLoading: startLoading });
|
|
248
|
+
const revalidating = ref3(false);
|
|
240
249
|
const abortController2 = ref3(null);
|
|
241
250
|
const globalAbort = useGlobalAbort ? useAbortController() : null;
|
|
242
251
|
let pollTimer = null;
|
|
@@ -253,11 +262,16 @@ function useApi(url, options = {}) {
|
|
|
253
262
|
};
|
|
254
263
|
const executeRequest = async (config) => {
|
|
255
264
|
const cacheOpts = normalizeCacheOptions(options.cache);
|
|
265
|
+
let isRevalidating = false;
|
|
256
266
|
if (cacheOpts) {
|
|
257
267
|
const cached = readCache(cacheOpts.id);
|
|
258
268
|
if (cached !== null) {
|
|
259
|
-
state.mutate(cached);
|
|
260
|
-
|
|
269
|
+
state.mutate(applySelect(cached));
|
|
270
|
+
if (!staleWhileRevalidate) {
|
|
271
|
+
return applySelect(cached);
|
|
272
|
+
}
|
|
273
|
+
isRevalidating = true;
|
|
274
|
+
revalidating.value = true;
|
|
261
275
|
}
|
|
262
276
|
}
|
|
263
277
|
if (pollTimer) clearTimeout(pollTimer);
|
|
@@ -271,15 +285,16 @@ function useApi(url, options = {}) {
|
|
|
271
285
|
const gs = globalAbort.getSignal();
|
|
272
286
|
if (!gs.aborted) {
|
|
273
287
|
subscribedSignal = gs;
|
|
274
|
-
const currentCount = globalAbort.abortCount.value;
|
|
275
288
|
globalAbortHandler = () => {
|
|
276
|
-
|
|
289
|
+
controller.abort("Cancelled by global abort");
|
|
277
290
|
};
|
|
278
291
|
gs.addEventListener("abort", globalAbortHandler);
|
|
279
292
|
}
|
|
280
293
|
}
|
|
281
|
-
|
|
282
|
-
|
|
294
|
+
if (!isRevalidating) {
|
|
295
|
+
onBefore?.();
|
|
296
|
+
state.setLoading(true);
|
|
297
|
+
}
|
|
283
298
|
state.setError(null);
|
|
284
299
|
let wasCancelled = false;
|
|
285
300
|
let retryCount = 0;
|
|
@@ -303,7 +318,8 @@ function useApi(url, options = {}) {
|
|
|
303
318
|
signal: controller.signal,
|
|
304
319
|
...{ authMode: config?.authMode || authMode }
|
|
305
320
|
});
|
|
306
|
-
|
|
321
|
+
const selected = applySelect(response.data);
|
|
322
|
+
state.mutate(selected, response);
|
|
307
323
|
state.setStatusCode(response.status);
|
|
308
324
|
if (cacheOpts) {
|
|
309
325
|
writeCache(cacheOpts.id, response.data, cacheOpts.staleTime);
|
|
@@ -312,7 +328,7 @@ function useApi(url, options = {}) {
|
|
|
312
328
|
invalidateCache(options.invalidateCache);
|
|
313
329
|
}
|
|
314
330
|
onSuccess?.(response);
|
|
315
|
-
return
|
|
331
|
+
return selected;
|
|
316
332
|
} catch (err) {
|
|
317
333
|
if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
|
|
318
334
|
wasCancelled = true;
|
|
@@ -354,8 +370,9 @@ function useApi(url, options = {}) {
|
|
|
354
370
|
return null;
|
|
355
371
|
} finally {
|
|
356
372
|
if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
|
|
373
|
+
revalidating.value = false;
|
|
357
374
|
if (!wasCancelled) {
|
|
358
|
-
state.setLoading(false);
|
|
375
|
+
if (!isRevalidating) state.setLoading(false);
|
|
359
376
|
onFinish?.();
|
|
360
377
|
const { interval, whenHidden } = getPollConfig();
|
|
361
378
|
if (interval > 0) {
|
|
@@ -439,8 +456,10 @@ function useApi(url, options = {}) {
|
|
|
439
456
|
}
|
|
440
457
|
}, { deep: true });
|
|
441
458
|
}
|
|
442
|
-
return { ...state, execute, abort, reset, ignoreUpdates };
|
|
459
|
+
return { ...state, revalidating, execute, abort, reset, ignoreUpdates };
|
|
443
460
|
}
|
|
461
|
+
|
|
462
|
+
// src/useApi.helpers.ts
|
|
444
463
|
function useApiGet(url, options) {
|
|
445
464
|
return useApi(url, { ...options, method: "GET" });
|
|
446
465
|
}
|