@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 CHANGED
@@ -5,29 +5,71 @@
5
5
  [![Vue 3](https://img.shields.io/badge/Vue-3.x-green.svg?style=flat-square)](https://vuejs.org/)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-Included-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
7
7
 
8
- **Type-safe, feature-rich Axios wrapper for Vue 3 Composition API. Built for real-world business logic.**
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
- - ๐ŸŽฏ **Fully Type-Safe** โ€” End-to-end TypeScript support with strict typing for requests and responses
18
- - ๐Ÿ”„ **Smart Reactivity** โ€” Watch refs and automatically refetch when dependencies change
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** โ€” Atomic updates without triggering intermediate requests
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 โ€” Atomic Updates Without Refetch](#ignoreupdates--atomic-updates-without-refetch)
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
- Watch refs and automatically refetch when they change. Perfect for filters, search, and dynamic content.
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
- interface User {
353
- id: number
354
- name: string
355
- }
356
-
357
- const userId = ref(1)
394
+ const search = ref('')
395
+ const page = ref(1)
358
396
 
359
- const { data } = useApi<User>(
360
- () => `/users/${userId.value}`,
361
- { watch: userId, immediate: true }
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
- userId.value = 2 // โ†’ automatic refetch
403
+ // Change any dep โ†’ request re-fires automatically
404
+ search.value = 'shoes'
405
+ page.value = 2
365
406
  ```
366
407
 
367
- #### Multiple Dependencies
408
+ **Reactive URL:**
368
409
  ```typescript
369
- import { ref } from 'vue'
370
- import { useApi } from '@ametie/vue-muza-use'
410
+ const userId = ref(1)
371
411
 
372
- const searchQuery = ref('')
373
- const category = ref('all')
412
+ const { data } = useApi(() => `/users/${userId.value}`, {
413
+ immediate: true,
414
+ })
374
415
 
375
- const { data } = useApi(
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
- #### Auto-Save Form
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
- import { ref } from 'vue'
387
- import { useApi } from '@ametie/vue-muza-use'
424
+ const form = ref({ name: '', email: '' })
388
425
 
389
- const settings = ref({
390
- theme: 'dark',
391
- notifications: true
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
- useApi('/user/settings', {
395
- method: 'PUT',
396
- data: settings,
397
- watch: settings,
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
- import { ref } from 'vue'
415
- import { useApi } from '@ametie/vue-muza-use'
416
-
417
- const page = ref(1)
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
- // BAD: each assignment triggers a separate request
426
- page.value = 1 // โ†’ request 1: page=1, search=''
427
- search.value = 'john' // โ†’ request 2: page=1, search=john
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
- #### โœ… With ignoreUpdates โ€” 1 request fires
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
- import { ref } from 'vue'
434
- import { useApi } from '@ametie/vue-muza-use'
458
+ // Only one request fires (Vue batches sync changes)
459
+ status.value = 'active'
460
+ page.value = 1
461
+ ```
435
462
 
436
- const page = ref(1)
437
- const search = ref('')
463
+ ---
438
464
 
439
- const { execute, ignoreUpdates } = useApi('/users', {
440
- params: { page, search },
441
- watch: [page, search]
442
- })
465
+ ### ignoreUpdates โ€” Update Without Re-fetching
443
466
 
444
- // GOOD: all changes are batched, only one request fires
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
- #### Reset filters without auto-fetching
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
- Use `ignoreUpdates` to reset all filters to their defaults, then manually trigger a single request.
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 { execute, ignoreUpdates } = useApi('/users', {
465
- params: { page, search, status },
466
- watch: [page, search, status]
479
+ const { data, ignoreUpdates } = useApi('/products', {
480
+ params: () => ({ q: search.value }),
481
+ debounce: 300,
467
482
  })
468
483
 
469
- function resetFilters() {
484
+ function clearSearch() {
470
485
  ignoreUpdates(() => {
471
- page.value = 1
472
486
  search.value = ''
473
- status.value = 'all'
474
487
  })
475
- execute() // single request with reset values
488
+ // auto-tracking is paused โ€” no request fires
476
489
  }
477
490
  ```
478
491
 
479
- #### Safe to call without a watch option
492
+ The user types โ†’ auto-tracking fires โ†’ debounced request. Clicking "Clear" resets the input without triggering a fetch.
480
493
 
481
- If no `watch` option is configured, `ignoreUpdates` still runs the updater โ€” it just has nothing to suppress.
494
+ #### Safe to call when `lazy: true`
482
495
 
483
- ```typescript
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
- const counter = ref(0)
498
+ ```typescript
488
499
  const { ignoreUpdates } = useApi('/data')
489
500
 
490
- // Safe โ€” no error thrown, updater still runs
491
501
  ignoreUpdates(() => {
492
- counter.value = 42
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. If you need to update async values, update them outside
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 + watch
622
+ #### cache + auto-tracking
614
623
 
615
- When `watch` is configured, each watch-triggered `execute()` still checks the cache first:
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
- watch: categoryId,
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 in `onSuccess`
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
- const { mutate } = useApi<User[]>('/users', { immediate: true })
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<T, D>(url, options)`
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<T, D>` | Configuration object (see below). |
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
- | `watch` | `WatchSource \| WatchSource[]` | `undefined` | One or more refs to watch โ€” request re-fires when any of them change |
1692
- | `debounce` | `number` | `0` | Milliseconds to wait after the last watch change before firing the request |
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` | `T` | `null` | Initial value for `data` before the first request completes |
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<T \| null>` | Response data from the last successful request |
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<T> \| null>` | Full Axios response object including headers |
1754
- | `execute(config?)` | `(config?: ApiRequestConfig<D>) => Promise<T \| null>` | Manually trigger the request, optionally overriding options |
1755
- | `mutate(newData)` | `(newData: T \| null \| ((prev: T \| null) => T \| null)) => void` | Update `data` locally without a network request; clears `error` |
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 watch-based re-execution |
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
- Full component: debounced search that resets cleanly without triggering an intermediate request.
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, execute, ignoreUpdates } = useApi<User[]>(
2018
- () => `/users?search=${search.value}&page=${page.value}`,
2019
- {
2020
- watch: [search, page],
2021
- debounce: 400,
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 resetSearch() {
2228
+ function clearSearch() {
2027
2229
  ignoreUpdates(() => {
2028
2230
  search.value = ''
2029
- page.value = 1
2030
2231
  })
2031
- execute() // single request with reset values
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="resetSearch">Clear</button>
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 using `ignoreUpdates` so only one request fires.
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, execute, ignoreUpdates } = useApi<Post[]>(
2071
- () => `/posts?status=${status.value}&page=${page.value}`,
2072
- { watch: [status, page], immediate: true }
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
- // Reset page to 1 when filter changes โ€” one request, not two
2077
- ignoreUpdates(() => {
2078
- status.value = newStatus
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
- id: string
2290
- status: 'pending' | 'processing' | 'complete' | 'failed'
2291
- progress: number
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
- const { data: job, error } = useApi<JobStatus>(
2298
- () => jobId.value ? `/jobs/${jobId.value}` : undefined,
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` AND `watch` on a ref both trigger on setup | Use only `immediate` OR `watch` for the first load โ€” not both |
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 |