@ametie/vue-muza-use 0.4.9 โ 0.5.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 +473 -552
- package/dist/index.cjs +7 -2
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.mjs +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,18 +13,48 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
13
13
|
|
|
14
14
|
## โจ Features
|
|
15
15
|
|
|
16
|
+
**Core Features** (Get started in minutes):
|
|
16
17
|
- ๐ฏ **Fully Type-Safe** โ End-to-end TypeScript support with strict typing for requests and responses
|
|
17
18
|
- ๐ **Smart Reactivity** โ Watch refs and automatically refetch when dependencies change
|
|
18
19
|
- โฑ๏ธ **Built-in Debouncing** โ Perfect for search inputs and auto-save forms
|
|
19
20
|
- ๐ก๏ธ **Race Condition Protection** โ Global abort controller cancels stale requests automatically
|
|
20
|
-
- ๐ **JWT Token Management** โ Automatic token refresh with request queueing on 401 responses
|
|
21
|
-
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic that respects component unmounting
|
|
22
21
|
- ๐ **Auto-Polling** โ Built-in interval fetching with smart tab visibility detection
|
|
23
22
|
- ๐งน **Zero Memory Leaks** โ Automatic cleanup of pending requests on component unmount
|
|
23
|
+
|
|
24
|
+
**Advanced Features** (When you need them):
|
|
25
|
+
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic that respects component unmounting
|
|
26
|
+
- ๐ **JWT Token Management** โ Automatic token refresh with request queueing on 401 responses
|
|
24
27
|
- ๐๏ธ **Flexible Architecture** โ Bring your own Axios instance with full configuration control
|
|
25
28
|
|
|
26
29
|
---
|
|
27
30
|
|
|
31
|
+
## ๐ Table of Contents
|
|
32
|
+
|
|
33
|
+
**Getting Started:**
|
|
34
|
+
- [Installation](#-installation)
|
|
35
|
+
- [Quick Start](#-quick-start)
|
|
36
|
+
- [Basic Usage](#-basic-usage)
|
|
37
|
+
|
|
38
|
+
**Core Features:**
|
|
39
|
+
- [Watch & Auto-Refetch](#watch--auto-refetch)
|
|
40
|
+
- [Polling](#polling-background-updates)
|
|
41
|
+
- [Error Handling](#error-handling)
|
|
42
|
+
- [Loading States](#loading-states)
|
|
43
|
+
- [Manual Data Updates](#manual-data-updates)
|
|
44
|
+
|
|
45
|
+
**Real-World Examples:**
|
|
46
|
+
- [Data Table with Pagination](#data-table-with-pagination--sorting)
|
|
47
|
+
- [Request Cancellation](#request-cancellation)
|
|
48
|
+
|
|
49
|
+
**Advanced:**
|
|
50
|
+
- [Custom Axios Instance](#-advanced-configuration)
|
|
51
|
+
- [Authentication & Tokens](#-authentication--token-management) *(Optional)*
|
|
52
|
+
- [API Reference](#-api-reference)
|
|
53
|
+
|
|
54
|
+
> ๐ก **New to the library?** Start with [Quick Start](#-quick-start), then explore [Basic Usage](#-basic-usage). Skip authentication until you need it!
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
28
58
|
## ๐ฆ Installation
|
|
29
59
|
|
|
30
60
|
```bash
|
|
@@ -40,114 +70,10 @@ yarn add @ametie/vue-muza-use axios
|
|
|
40
70
|
|
|
41
71
|
---
|
|
42
72
|
|
|
43
|
-
## ๐ Token Management
|
|
44
|
-
|
|
45
|
-
The library supports **automatic token refresh** with two security modes:
|
|
46
|
-
|
|
47
|
-
### Mode 1: localStorage (Default)
|
|
48
|
-
```typescript
|
|
49
|
-
const api = createApiClient({
|
|
50
|
-
baseURL: 'https://api.example.com',
|
|
51
|
-
authOptions: {
|
|
52
|
-
refreshUrl: '/auth/refresh',
|
|
53
|
-
refreshWithCredentials: false, // or omit (default)
|
|
54
|
-
onTokenRefreshFailed: () => router.push('/login')
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
```
|
|
58
|
-
- โ
Both `accessToken` and `refreshToken` stored in localStorage
|
|
59
|
-
- โ ๏ธ Less secure (vulnerable to XSS), but easier to implement
|
|
60
|
-
|
|
61
|
-
### Mode 2: httpOnly Cookies (Recommended for Production)
|
|
62
|
-
```typescript
|
|
63
|
-
const api = createApiClient({
|
|
64
|
-
baseURL: 'https://api.example.com',
|
|
65
|
-
authOptions: {
|
|
66
|
-
refreshUrl: '/auth/refresh',
|
|
67
|
-
refreshWithCredentials: true, // ๐ Key parameter!
|
|
68
|
-
onTokenRefreshFailed: () => router.push('/login')
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
```
|
|
72
|
-
- โ
Only `accessToken` in localStorage
|
|
73
|
-
- ๐ `refreshToken` sent via httpOnly cookie (XSS protection)
|
|
74
|
-
- ๐ก Backend must set: `Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=None` (for cross-origin)
|
|
75
|
-
- ๐ฏ **Smart Auto-Detection**: If no `refreshToken` in localStorage, `withCredentials: true` is automatically enabled
|
|
76
|
-
|
|
77
|
-
**โ ๏ธ Common Issues & Solutions:**
|
|
78
|
-
|
|
79
|
-
1. **Cookie not sent to backend?**
|
|
80
|
-
- Check: Browser DevTools โ Application โ Cookies
|
|
81
|
-
- Cookie must have: `HttpOnly`, `Secure` (for HTTPS), `SameSite=None` (for cross-origin)
|
|
82
|
-
- Cookie domain must match your request domain
|
|
83
|
-
|
|
84
|
-
2. **CORS errors?**
|
|
85
|
-
- Backend must set: `Access-Control-Allow-Credentials: true`
|
|
86
|
-
- Backend must set specific origin (NOT `*`): `Access-Control-Allow-Origin: https://your-frontend.com`
|
|
87
|
-
- Frontend baseURL must match backend domain
|
|
88
|
-
|
|
89
|
-
3. **401 on refresh?**
|
|
90
|
-
- Check Network tab โ Refresh request โ Headers
|
|
91
|
-
- Verify "Cookie" header includes your refresh token
|
|
92
|
-
- Backend logs: does it receive the cookie?
|
|
93
|
-
|
|
94
|
-
### Custom Refresh Payload
|
|
95
|
-
Send additional data with refresh requests:
|
|
96
|
-
|
|
97
|
-
**โ ๏ธ IMPORTANT:** Use functions for dynamic data (like tokens from storage):
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const api = createApiClient({
|
|
101
|
-
baseURL: 'https://api.example.com',
|
|
102
|
-
authOptions: {
|
|
103
|
-
refreshUrl: '/auth/refresh',
|
|
104
|
-
// โ WRONG - computed once at app start
|
|
105
|
-
// refreshPayload: { refreshToken: tokenManager.getRefreshToken() }
|
|
106
|
-
|
|
107
|
-
// โ
CORRECT - computed on each refresh
|
|
108
|
-
refreshPayload: () => ({
|
|
109
|
-
refreshToken: tokenManager.getRefreshToken(),
|
|
110
|
-
timestamp: Date.now()
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Static payload (for constants only):
|
|
117
|
-
```typescript
|
|
118
|
-
refreshPayload: {
|
|
119
|
-
deviceId: 'mobile-v1',
|
|
120
|
-
platform: 'ios' // constants that don't change
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Dynamic function:
|
|
125
|
-
```typescript
|
|
126
|
-
refreshPayload: () => ({
|
|
127
|
-
timestamp: Date.now(),
|
|
128
|
-
sessionId: getSessionId() // reads current value
|
|
129
|
-
})
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Saving Tokens After Login
|
|
133
|
-
```typescript
|
|
134
|
-
import { tokenManager } from '@ametie/vue-muza-use'
|
|
135
|
-
|
|
136
|
-
const { execute } = useApiPost<AuthResponse>('/auth/login', {
|
|
137
|
-
authMode: 'public',
|
|
138
|
-
onSuccess(response) {
|
|
139
|
-
tokenManager.setTokens({
|
|
140
|
-
accessToken: response.data.accessToken,
|
|
141
|
-
refreshToken: response.data.refreshToken, // optional in cookie mode
|
|
142
|
-
expiresIn: response.data.expiresIn
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
```
|
|
147
|
-
---
|
|
148
|
-
|
|
149
73
|
## ๐ Quick Start
|
|
150
74
|
|
|
75
|
+
Get started in 2 minutes with minimal configuration.
|
|
76
|
+
|
|
151
77
|
### 1. Setup Plugin (`main.ts`)
|
|
152
78
|
|
|
153
79
|
```typescript
|
|
@@ -157,30 +83,20 @@ import App from './App.vue'
|
|
|
157
83
|
|
|
158
84
|
const app = createApp(App)
|
|
159
85
|
|
|
160
|
-
// Create
|
|
86
|
+
// Create API client with minimal config
|
|
161
87
|
const api = createApiClient({
|
|
162
|
-
baseURL: 'https://api.example.com'
|
|
163
|
-
withAuth: true, // Automatic token injection
|
|
164
|
-
authOptions: {
|
|
165
|
-
refreshUrl: '/auth/refresh',
|
|
166
|
-
onTokenRefreshFailed: () => {
|
|
167
|
-
window.location.href = '/login'
|
|
168
|
-
}
|
|
169
|
-
}
|
|
88
|
+
baseURL: 'https://api.example.com'
|
|
170
89
|
})
|
|
171
90
|
|
|
172
91
|
// Install plugin
|
|
173
|
-
app.use(createApi({
|
|
174
|
-
axios: api,
|
|
175
|
-
onError: (error) => {
|
|
176
|
-
console.error('API Error:', error.message)
|
|
177
|
-
}
|
|
178
|
-
}))
|
|
92
|
+
app.use(createApi({ axios: api }))
|
|
179
93
|
|
|
180
94
|
app.mount('#app')
|
|
181
95
|
```
|
|
182
96
|
|
|
183
|
-
|
|
97
|
+
> ๐ก **That's it!** No auth configuration needed to get started. Add it later when you need it.
|
|
98
|
+
|
|
99
|
+
### 2. Your First Request
|
|
184
100
|
|
|
185
101
|
```vue
|
|
186
102
|
<script setup lang="ts">
|
|
@@ -193,7 +109,7 @@ interface User {
|
|
|
193
109
|
}
|
|
194
110
|
|
|
195
111
|
const { data, loading, error } = useApi<User>('/users/1', {
|
|
196
|
-
immediate: true
|
|
112
|
+
immediate: true // Auto-fetch on mount
|
|
197
113
|
})
|
|
198
114
|
</script>
|
|
199
115
|
|
|
@@ -207,29 +123,23 @@ const { data, loading, error } = useApi<User>('/users/1', {
|
|
|
207
123
|
</template>
|
|
208
124
|
```
|
|
209
125
|
|
|
210
|
-
### 3.
|
|
126
|
+
### 3. Live Search with Debounce
|
|
211
127
|
|
|
212
|
-
This
|
|
128
|
+
This example shows the library's power: **automatic race condition handling** and **debouncing** built-in.
|
|
213
129
|
|
|
214
130
|
```vue
|
|
215
131
|
<script setup lang="ts">
|
|
216
132
|
import { ref } from 'vue'
|
|
217
133
|
import { useApi } from '@ametie/vue-muza-use'
|
|
218
134
|
|
|
219
|
-
interface Product {
|
|
220
|
-
id: number
|
|
221
|
-
name: string
|
|
222
|
-
price: number
|
|
223
|
-
}
|
|
224
|
-
|
|
225
135
|
const searchQuery = ref('')
|
|
226
136
|
|
|
227
|
-
// ๐ก
|
|
228
|
-
const { data, loading } = useApi
|
|
137
|
+
// ๐ก Use a getter function for dynamic URLs
|
|
138
|
+
const { data, loading } = useApi(
|
|
229
139
|
() => `/products/search?q=${searchQuery.value}`,
|
|
230
140
|
{
|
|
231
|
-
watch: searchQuery,
|
|
232
|
-
debounce: 500
|
|
141
|
+
watch: searchQuery, // Auto-refetch when query changes
|
|
142
|
+
debounce: 500 // Wait 500ms after typing stops
|
|
233
143
|
}
|
|
234
144
|
)
|
|
235
145
|
</script>
|
|
@@ -247,274 +157,145 @@ const { data, loading } = useApi<Product[]>(
|
|
|
247
157
|
</template>
|
|
248
158
|
```
|
|
249
159
|
|
|
160
|
+
**๐ฏ What just happened?**
|
|
161
|
+
- No race conditions โ previous searches auto-cancel
|
|
162
|
+
- Debounce โ waits for user to stop typing
|
|
163
|
+
- TypeScript โ full type safety
|
|
164
|
+
- Clean code โ no manual cleanup needed
|
|
165
|
+
|
|
250
166
|
---
|
|
251
167
|
|
|
252
|
-
## ๐
|
|
168
|
+
## ๐ Basic Usage
|
|
253
169
|
|
|
254
170
|
### GET Requests
|
|
255
171
|
|
|
256
|
-
####
|
|
172
|
+
#### Manual Execution
|
|
257
173
|
```typescript
|
|
258
174
|
const { data, loading, error, execute } = useApi<User>('/users/1')
|
|
259
175
|
|
|
260
|
-
//
|
|
176
|
+
// Trigger manually (e.g., on button click)
|
|
261
177
|
await execute()
|
|
262
178
|
```
|
|
263
179
|
|
|
264
180
|
#### Auto-Fetch on Mount
|
|
265
181
|
```typescript
|
|
266
|
-
useApi<User>('/users/1', {
|
|
267
|
-
immediate: true
|
|
268
|
-
retry: 3, // Retry 3 times on network failure
|
|
269
|
-
retryDelay: 1000 // Wait 1s between retries
|
|
270
|
-
})
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### Dynamic URLs
|
|
274
|
-
Use a **getter function** for reactive URLs - simple and efficient:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
const userId = ref(1)
|
|
278
|
-
|
|
279
|
-
// โ
Preferred: Getter function (no computed needed!)
|
|
280
|
-
const { data } = useApi(() => `/users/${userId.value}`, {
|
|
281
|
-
watch: userId,
|
|
282
|
-
immediate: true
|
|
182
|
+
const { data } = useApi<User>('/users/1', {
|
|
183
|
+
immediate: true // Fetches automatically
|
|
283
184
|
})
|
|
284
|
-
|
|
285
|
-
// Also works: computed ref
|
|
286
|
-
const url = computed(() => `/users/${userId.value}`)
|
|
287
|
-
const { data } = useApi(url, { watch: userId, immediate: true })
|
|
288
185
|
```
|
|
289
186
|
|
|
290
|
-
#### Query Parameters
|
|
291
|
-
Pass `params` as static object or reactive ref:
|
|
292
|
-
|
|
187
|
+
#### With Query Parameters
|
|
293
188
|
```typescript
|
|
294
189
|
const filters = ref({
|
|
295
190
|
status: 'active',
|
|
296
|
-
sort: 'name',
|
|
297
191
|
limit: 20
|
|
298
192
|
})
|
|
299
193
|
|
|
300
194
|
const { data } = useApi('/users', {
|
|
301
|
-
params: filters,
|
|
302
|
-
watch: filters,
|
|
303
|
-
|
|
195
|
+
params: filters, // Automatically unwrapped
|
|
196
|
+
watch: filters, // Re-fetch when filters change
|
|
197
|
+
immediate: true
|
|
304
198
|
})
|
|
305
|
-
|
|
306
|
-
// URL becomes: /users?status=active&sort=name&limit=20
|
|
307
199
|
```
|
|
308
200
|
|
|
309
201
|
---
|
|
310
202
|
|
|
311
203
|
### POST/PUT/PATCH Requests
|
|
312
204
|
|
|
313
|
-
#### Form Submission
|
|
205
|
+
#### Simple Form Submission
|
|
314
206
|
```vue
|
|
315
207
|
<script setup lang="ts">
|
|
316
208
|
import { ref } from 'vue'
|
|
317
209
|
import { useApi } from '@ametie/vue-muza-use'
|
|
318
210
|
|
|
319
|
-
|
|
320
|
-
email: string
|
|
321
|
-
password: string
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
interface LoginResponse {
|
|
325
|
-
token: string
|
|
326
|
-
user: { id: number; name: string }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const form = ref<LoginForm>({
|
|
211
|
+
const form = ref({
|
|
330
212
|
email: '',
|
|
331
213
|
password: ''
|
|
332
214
|
})
|
|
333
215
|
|
|
334
|
-
const { execute, loading, error } = useApi
|
|
216
|
+
const { execute, loading, error } = useApi('/auth/login', {
|
|
335
217
|
method: 'POST',
|
|
336
|
-
|
|
337
|
-
data: form, // Ref is auto-unwrapped
|
|
218
|
+
data: form, // Ref is auto-unwrapped
|
|
338
219
|
onSuccess: (response) => {
|
|
339
|
-
|
|
340
|
-
router.push('/dashboard')
|
|
220
|
+
console.log('Logged in!', response.data)
|
|
341
221
|
}
|
|
342
222
|
})
|
|
343
|
-
|
|
344
|
-
const handleSubmit = async () => {
|
|
345
|
-
await execute()
|
|
346
|
-
}
|
|
347
223
|
</script>
|
|
348
224
|
|
|
349
225
|
<template>
|
|
350
|
-
<form @submit.prevent="
|
|
351
|
-
<input v-model="form.email" type="email"
|
|
352
|
-
<input v-model="form.password" type="password"
|
|
353
|
-
|
|
354
|
-
<button type="submit" :disabled="loading">
|
|
226
|
+
<form @submit.prevent="execute">
|
|
227
|
+
<input v-model="form.email" type="email" />
|
|
228
|
+
<input v-model="form.password" type="password" />
|
|
229
|
+
<button :disabled="loading">
|
|
355
230
|
{{ loading ? 'Signing in...' : 'Sign In' }}
|
|
356
231
|
</button>
|
|
357
|
-
|
|
358
|
-
<p v-if="error" class="error">{{ error.message }}</p>
|
|
232
|
+
<p v-if="error">{{ error.message }}</p>
|
|
359
233
|
</form>
|
|
360
234
|
</template>
|
|
361
235
|
```
|
|
362
236
|
|
|
363
|
-
#### Update with Optimistic UI
|
|
364
|
-
```typescript
|
|
365
|
-
const { execute: updateProfile } = useApi('/user/profile', {
|
|
366
|
-
method: 'PUT',
|
|
367
|
-
onBefore: () => {
|
|
368
|
-
// Show optimistic update
|
|
369
|
-
localProfile.value = { ...localProfile.value, ...changes }
|
|
370
|
-
},
|
|
371
|
-
onSuccess: (response) => {
|
|
372
|
-
toast.success('Profile updated!')
|
|
373
|
-
},
|
|
374
|
-
onError: () => {
|
|
375
|
-
// Rollback on error
|
|
376
|
-
localProfile.value = originalProfile
|
|
377
|
-
}
|
|
378
|
-
})
|
|
379
|
-
```
|
|
380
|
-
|
|
381
237
|
---
|
|
382
238
|
|
|
383
|
-
|
|
239
|
+
## ๐ฏ Core Features
|
|
384
240
|
|
|
385
|
-
Watch
|
|
241
|
+
### Watch & Auto-Refetch
|
|
386
242
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
243
|
+
Watch refs and automatically refetch when they change. Perfect for filters, search, and dynamic content.
|
|
244
|
+
|
|
245
|
+
#### Single Dependency
|
|
246
|
+
```typescript
|
|
247
|
+
const userId = ref(1)
|
|
248
|
+
|
|
249
|
+
const { data } = useApi(() => `/users/${userId.value}`, {
|
|
250
|
+
watch: userId,
|
|
251
|
+
immediate: true
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Change userId โ automatic refetch
|
|
255
|
+
userId.value = 2
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Multiple Dependencies
|
|
259
|
+
```typescript
|
|
390
260
|
const searchQuery = ref('')
|
|
391
261
|
const category = ref('all')
|
|
392
262
|
|
|
393
|
-
|
|
394
|
-
const { data, loading } = useApi<Product[]>(
|
|
263
|
+
const { data } = useApi(
|
|
395
264
|
() => `/products?q=${searchQuery.value}&category=${category.value}`,
|
|
396
265
|
{
|
|
397
|
-
watch: [searchQuery, category],
|
|
398
|
-
debounce: 500
|
|
266
|
+
watch: [searchQuery, category],
|
|
267
|
+
debounce: 500
|
|
399
268
|
}
|
|
400
269
|
)
|
|
401
|
-
</script>
|
|
402
|
-
|
|
403
|
-
<template>
|
|
404
|
-
<input v-model="searchQuery" placeholder="Search..." />
|
|
405
|
-
<select v-model="category">
|
|
406
|
-
<option value="all">All</option>
|
|
407
|
-
<option value="electronics">Electronics</option>
|
|
408
|
-
</select>
|
|
409
|
-
|
|
410
|
-
<ProductList :products="data" :loading="loading" />
|
|
411
|
-
</template>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
#### Data Table with Pagination & Sorting
|
|
415
|
-
```vue
|
|
416
|
-
<script setup lang="ts">
|
|
417
|
-
interface PaginatedResponse<T> {
|
|
418
|
-
data: T[]
|
|
419
|
-
total: number
|
|
420
|
-
page: number
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const page = ref(1)
|
|
424
|
-
const sortBy = ref('created_at')
|
|
425
|
-
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
426
|
-
|
|
427
|
-
const params = computed(() => ({
|
|
428
|
-
page: page.value,
|
|
429
|
-
sort_by: sortBy.value,
|
|
430
|
-
sort_order: sortOrder.value,
|
|
431
|
-
per_page: 20
|
|
432
|
-
}))
|
|
433
|
-
|
|
434
|
-
const { data, loading } = useApi<PaginatedResponse<Order>>('/orders', {
|
|
435
|
-
params,
|
|
436
|
-
watch: params, // Auto-refetch when any param changes
|
|
437
|
-
immediate: true
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
const handleSort = (column: string) => {
|
|
441
|
-
if (sortBy.value === column) {
|
|
442
|
-
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
|
|
443
|
-
} else {
|
|
444
|
-
sortBy.value = column
|
|
445
|
-
sortOrder.value = 'desc'
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
</script>
|
|
449
|
-
|
|
450
|
-
<template>
|
|
451
|
-
<table>
|
|
452
|
-
<thead>
|
|
453
|
-
<tr>
|
|
454
|
-
<th @click="handleSort('id')">ID</th>
|
|
455
|
-
<th @click="handleSort('created_at')">Date</th>
|
|
456
|
-
<th @click="handleSort('total')">Total</th>
|
|
457
|
-
</tr>
|
|
458
|
-
</thead>
|
|
459
|
-
<tbody v-if="!loading">
|
|
460
|
-
<tr v-for="order in data?.data" :key="order.id">
|
|
461
|
-
<td>{{ order.id }}</td>
|
|
462
|
-
<td>{{ order.created_at }}</td>
|
|
463
|
-
<td>${{ order.total }}</td>
|
|
464
|
-
</tr>
|
|
465
|
-
</tbody>
|
|
466
|
-
</table>
|
|
467
|
-
|
|
468
|
-
<Pagination
|
|
469
|
-
v-model="page"
|
|
470
|
-
:total="data?.total"
|
|
471
|
-
:loading="loading"
|
|
472
|
-
/>
|
|
473
|
-
</template>
|
|
474
270
|
```
|
|
475
271
|
|
|
476
272
|
#### Auto-Save Form
|
|
477
273
|
```typescript
|
|
478
274
|
const settings = ref({
|
|
479
275
|
theme: 'dark',
|
|
480
|
-
notifications: true
|
|
481
|
-
language: 'en'
|
|
276
|
+
notifications: true
|
|
482
277
|
})
|
|
483
278
|
|
|
484
279
|
useApi('/user/settings', {
|
|
485
280
|
method: 'PUT',
|
|
486
281
|
data: settings,
|
|
487
|
-
watch: settings,
|
|
488
|
-
debounce: 1000,
|
|
489
|
-
onSuccess: () =>
|
|
490
|
-
toast.success('Settings saved')
|
|
491
|
-
}
|
|
282
|
+
watch: settings, // Deep watch by default
|
|
283
|
+
debounce: 1000, // Save 1s after changes stop
|
|
284
|
+
onSuccess: () => toast.success('Saved!')
|
|
492
285
|
})
|
|
493
286
|
```
|
|
494
287
|
|
|
495
288
|
---
|
|
496
289
|
|
|
497
|
-
###
|
|
290
|
+
### Polling (Background Updates)
|
|
498
291
|
|
|
499
|
-
Keep data fresh with
|
|
292
|
+
Keep data fresh with smart polling. Automatically pauses when browser tab is hidden.
|
|
500
293
|
|
|
501
294
|
#### Simple Polling
|
|
502
295
|
```typescript
|
|
503
|
-
|
|
504
|
-
const { data } = useApi<Notification[]>('/notifications', {
|
|
505
|
-
immediate: true,
|
|
506
|
-
poll: 5000
|
|
507
|
-
})
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
#### Smart Polling with Controls
|
|
511
|
-
```typescript
|
|
512
|
-
const { data, loading } = useApi('/stock-prices', {
|
|
296
|
+
const { data } = useApi('/notifications', {
|
|
513
297
|
immediate: true,
|
|
514
|
-
poll:
|
|
515
|
-
interval: 1000,
|
|
516
|
-
whenHidden: false // โ ๏ธ Pause when tab is hidden (default)
|
|
517
|
-
}
|
|
298
|
+
poll: 5000 // Fetch every 5 seconds
|
|
518
299
|
})
|
|
519
300
|
```
|
|
520
301
|
|
|
@@ -522,71 +303,249 @@ const { data, loading } = useApi('/stock-prices', {
|
|
|
522
303
|
```typescript
|
|
523
304
|
const pollInterval = ref(3000)
|
|
524
305
|
|
|
525
|
-
// Start/stop polling by changing the ref
|
|
526
306
|
const { data } = useApi('/live-feed', {
|
|
527
307
|
poll: pollInterval,
|
|
528
308
|
immediate: true
|
|
529
309
|
})
|
|
530
310
|
|
|
531
311
|
// Stop polling
|
|
532
|
-
|
|
312
|
+
pollInterval.value = 0
|
|
533
313
|
|
|
534
|
-
// Resume with
|
|
535
|
-
|
|
314
|
+
// Resume with different interval
|
|
315
|
+
pollInterval.value = 5000
|
|
536
316
|
```
|
|
537
317
|
|
|
538
318
|
---
|
|
539
319
|
|
|
540
|
-
###
|
|
320
|
+
### Error Handling
|
|
541
321
|
|
|
542
|
-
####
|
|
543
|
-
|
|
322
|
+
#### Per-Request Error Handling
|
|
323
|
+
```typescript
|
|
324
|
+
const { error, execute } = useApi('/users', {
|
|
325
|
+
onError: (error) => {
|
|
326
|
+
if (error.status === 404) {
|
|
327
|
+
toast.error('User not found')
|
|
328
|
+
} else {
|
|
329
|
+
toast.error('Something went wrong')
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
skipErrorNotification: true // Skip global handler
|
|
333
|
+
})
|
|
334
|
+
```
|
|
544
335
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
336
|
+
#### Retry on Failure
|
|
337
|
+
```typescript
|
|
338
|
+
useApi('/flaky-endpoint', {
|
|
339
|
+
immediate: true,
|
|
340
|
+
retry: 3, // Retry 3 times
|
|
341
|
+
retryDelay: 1000 // Wait 1s between retries
|
|
342
|
+
})
|
|
343
|
+
```
|
|
548
344
|
|
|
549
|
-
|
|
345
|
+
---
|
|
550
346
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
347
|
+
### Loading States
|
|
348
|
+
|
|
349
|
+
#### Per-Request Loading
|
|
350
|
+
```typescript
|
|
351
|
+
const { data: user, loading: userLoading } = useApi('/user')
|
|
352
|
+
const { data: posts, loading: postsLoading } = useApi('/posts')
|
|
353
|
+
|
|
354
|
+
// Each request tracks its own loading state
|
|
355
|
+
```
|
|
555
356
|
|
|
556
|
-
|
|
557
|
-
|
|
357
|
+
#### Lifecycle Hooks
|
|
358
|
+
```typescript
|
|
359
|
+
const { execute } = useApi('/analytics', {
|
|
360
|
+
onBefore: () => {
|
|
361
|
+
loadingBar.start()
|
|
362
|
+
},
|
|
363
|
+
onSuccess: (response) => {
|
|
364
|
+
console.log('Success!', response.data)
|
|
365
|
+
},
|
|
366
|
+
onError: (error) => {
|
|
367
|
+
console.error('Failed:', error.message)
|
|
368
|
+
},
|
|
369
|
+
onFinish: () => {
|
|
370
|
+
loadingBar.finish() // Always called
|
|
371
|
+
}
|
|
558
372
|
})
|
|
373
|
+
```
|
|
559
374
|
|
|
560
|
-
|
|
375
|
+
---
|
|
561
376
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
377
|
+
### Manual Data Updates
|
|
378
|
+
|
|
379
|
+
Use `setData` to manually update the data ref. Supports direct values or updater functions (like React's `setState`).
|
|
380
|
+
|
|
381
|
+
> ๐ **When to use `setData`:**
|
|
382
|
+
> โ
Adding/removing/updating items in arrays
|
|
383
|
+
> โ
Local sorting/filtering (without refetching)
|
|
384
|
+
> โ
Transform data in `onSuccess` (adding computed fields)
|
|
385
|
+
>
|
|
386
|
+
> **When to use `computed` instead:**
|
|
387
|
+
> โ
Completely changing data structure (e.g., API format โ App format)
|
|
388
|
+
> โ
Extracting nested data that changes the return type
|
|
389
|
+
> โ
Complex transformations that depend on other refs
|
|
390
|
+
|
|
391
|
+
#### Add/Remove/Update Items
|
|
392
|
+
```typescript
|
|
393
|
+
const { data, setData } = useApi<Todo[]>('/todos', { immediate: true })
|
|
394
|
+
|
|
395
|
+
// Add item
|
|
396
|
+
const addTodo = (newTodo: Todo) => {
|
|
397
|
+
setData(prev => prev ? [...prev, newTodo] : [newTodo])
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Remove item
|
|
401
|
+
const removeTodo = (id: number) => {
|
|
402
|
+
setData(prev => prev?.filter(t => t.id !== id) ?? null)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Update item
|
|
406
|
+
const updateTodo = (id: number, updates: Partial<Todo>) => {
|
|
407
|
+
setData(prev =>
|
|
408
|
+
prev?.map(t => t.id === id ? { ...t, ...updates } : t) ?? null
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### Sort/Filter Locally
|
|
414
|
+
```typescript
|
|
415
|
+
const { data, setData } = useApi<Product[]>('/products', { immediate: true })
|
|
416
|
+
|
|
417
|
+
const sortByPrice = () => {
|
|
418
|
+
setData(prev => prev ? [...prev].sort((a, b) => a.price - b.price) : null)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const filterActive = () => {
|
|
422
|
+
setData(prev => prev?.filter(p => p.active) ?? null)
|
|
565
423
|
}
|
|
566
424
|
|
|
567
|
-
//
|
|
568
|
-
const
|
|
569
|
-
|
|
425
|
+
// Reset to original
|
|
426
|
+
const resetFilters = () => execute()
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### Transform in `onSuccess`
|
|
430
|
+
|
|
431
|
+
Use `setData` in `onSuccess` to transform data right after fetching. Two approaches:
|
|
432
|
+
|
|
433
|
+
**Approach 1: Same type (recommended)**
|
|
434
|
+
```typescript
|
|
435
|
+
interface User {
|
|
436
|
+
id: number
|
|
437
|
+
firstName: string
|
|
438
|
+
lastName: string
|
|
439
|
+
fullName?: string // Optional field
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const { data, setData } = useApi<User[]>('/users', {
|
|
443
|
+
immediate: true,
|
|
444
|
+
onSuccess: ({ data: users }) => {
|
|
445
|
+
// Add computed field - still User[] type
|
|
446
|
+
setData(users.map(u => ({
|
|
447
|
+
...u,
|
|
448
|
+
fullName: `${u.firstName} ${u.lastName}`
|
|
449
|
+
})))
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Approach 2: Different structure (use separate computed)**
|
|
455
|
+
```typescript
|
|
456
|
+
interface ApiUser {
|
|
457
|
+
first_name: string
|
|
458
|
+
last_name: string
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// If API returns different structure, use computed for transformation
|
|
462
|
+
const { data: rawData } = useApi<ApiUser[]>('/users', { immediate: true })
|
|
463
|
+
|
|
464
|
+
const users = computed(() =>
|
|
465
|
+
rawData.value?.map(u => ({
|
|
466
|
+
firstName: u.first_name,
|
|
467
|
+
lastName: u.last_name,
|
|
468
|
+
fullName: `${u.first_name} ${u.last_name}`
|
|
469
|
+
})) ?? []
|
|
470
|
+
)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
> ๐ก **Rule of thumb:**
|
|
474
|
+
> - โ
**Use `setData` in `onSuccess`** if you're adding/modifying fields but keeping the same base type
|
|
475
|
+
> - โ
**Use `computed`** if you're completely changing the data structure (e.g., snake_case โ camelCase)
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## ๐ Real-World Examples
|
|
480
|
+
|
|
481
|
+
### Data Table with Pagination & Sorting
|
|
482
|
+
```vue
|
|
483
|
+
<script setup lang="ts">
|
|
484
|
+
const page = ref(1)
|
|
485
|
+
const sortBy = ref('created_at')
|
|
486
|
+
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
487
|
+
|
|
488
|
+
const params = computed(() => ({
|
|
489
|
+
page: page.value,
|
|
490
|
+
sort_by: sortBy.value,
|
|
491
|
+
sort_order: sortOrder.value,
|
|
492
|
+
per_page: 20
|
|
493
|
+
}))
|
|
494
|
+
|
|
495
|
+
const { data, loading } = useApi('/orders', {
|
|
496
|
+
params,
|
|
497
|
+
watch: params,
|
|
498
|
+
immediate: true
|
|
570
499
|
})
|
|
571
500
|
</script>
|
|
501
|
+
|
|
502
|
+
<template>
|
|
503
|
+
<table>
|
|
504
|
+
<thead>
|
|
505
|
+
<tr>
|
|
506
|
+
<th @click="sortBy = 'id'">ID</th>
|
|
507
|
+
<th @click="sortBy = 'created_at'">Date</th>
|
|
508
|
+
<th @click="sortBy = 'total'">Total</th>
|
|
509
|
+
</tr>
|
|
510
|
+
</thead>
|
|
511
|
+
<tbody v-if="!loading">
|
|
512
|
+
<tr v-for="order in data?.data" :key="order.id">
|
|
513
|
+
<td>{{ order.id }}</td>
|
|
514
|
+
<td>{{ order.created_at }}</td>
|
|
515
|
+
<td>${{ order.total }}</td>
|
|
516
|
+
</tr>
|
|
517
|
+
</tbody>
|
|
518
|
+
</table>
|
|
519
|
+
|
|
520
|
+
<Pagination v-model="page" :total="data?.total" />
|
|
521
|
+
</template>
|
|
572
522
|
```
|
|
573
523
|
|
|
574
|
-
|
|
524
|
+
|
|
525
|
+
### Request Cancellation
|
|
575
526
|
```typescript
|
|
576
|
-
|
|
527
|
+
import { useAbortController } from '@ametie/vue-muza-use'
|
|
577
528
|
|
|
578
|
-
|
|
579
|
-
execute()
|
|
529
|
+
const { abortAll } = useAbortController()
|
|
580
530
|
|
|
581
|
-
//
|
|
582
|
-
|
|
531
|
+
// Multiple requests
|
|
532
|
+
const { data: products } = useApi('/products', { params: filters })
|
|
533
|
+
const { data: stats } = useApi('/stats', { params: filters })
|
|
534
|
+
|
|
535
|
+
// Cancel all when filters reset
|
|
536
|
+
const resetFilters = () => {
|
|
537
|
+
abortAll() // ๐ Cancel both requests
|
|
538
|
+
filters.value = { /* defaults */ }
|
|
539
|
+
}
|
|
583
540
|
```
|
|
584
541
|
|
|
542
|
+
---
|
|
543
|
+
|
|
585
544
|
## โ๏ธ Advanced Configuration
|
|
586
545
|
|
|
587
546
|
### Custom Axios Instance
|
|
588
547
|
|
|
589
|
-
|
|
548
|
+
Full control over Axios configuration:
|
|
590
549
|
|
|
591
550
|
```typescript
|
|
592
551
|
import axios from 'axios'
|
|
@@ -595,14 +554,12 @@ import { createApi } from '@ametie/vue-muza-use'
|
|
|
595
554
|
const customAxios = axios.create({
|
|
596
555
|
baseURL: 'https://api.example.com',
|
|
597
556
|
timeout: 30000,
|
|
598
|
-
headers: {
|
|
599
|
-
'X-Custom-Header': 'value'
|
|
600
|
-
}
|
|
557
|
+
headers: { 'X-Custom-Header': 'value' }
|
|
601
558
|
})
|
|
602
559
|
|
|
603
|
-
// Add
|
|
560
|
+
// Add custom interceptors
|
|
604
561
|
customAxios.interceptors.request.use((config) => {
|
|
605
|
-
//
|
|
562
|
+
// Your logic
|
|
606
563
|
return config
|
|
607
564
|
})
|
|
608
565
|
|
|
@@ -611,42 +568,36 @@ app.use(createApi({ axios: customAxios }))
|
|
|
611
568
|
|
|
612
569
|
---
|
|
613
570
|
|
|
614
|
-
### Error
|
|
615
|
-
|
|
616
|
-
#### Custom Error Parser
|
|
571
|
+
### Global Error Handler
|
|
617
572
|
|
|
618
|
-
|
|
573
|
+
Normalize errors from different backend formats:
|
|
619
574
|
|
|
620
575
|
```typescript
|
|
621
|
-
import { createApi } from '@ametie/vue-muza-use'
|
|
622
|
-
|
|
623
576
|
app.use(createApi({
|
|
624
577
|
axios: api,
|
|
578
|
+
|
|
579
|
+
// Global error handler
|
|
580
|
+
onError: (error) => {
|
|
581
|
+
toast.error(error.message)
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
// Error parser (normalize backend responses)
|
|
625
585
|
errorParser: (error: any) => {
|
|
626
586
|
const response = error.response?.data
|
|
627
587
|
|
|
628
|
-
// Laravel
|
|
588
|
+
// Laravel validation errors
|
|
629
589
|
if (response?.errors) {
|
|
630
590
|
return {
|
|
631
591
|
message: 'Validation Failed',
|
|
632
592
|
status: error.response.status,
|
|
633
593
|
code: 'VALIDATION_ERROR',
|
|
634
|
-
errors: response.errors
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Custom API format
|
|
639
|
-
if (response?.error?.message) {
|
|
640
|
-
return {
|
|
641
|
-
message: response.error.message,
|
|
642
|
-
status: error.response?.status || 500,
|
|
643
|
-
code: response.error.code
|
|
594
|
+
errors: response.errors
|
|
644
595
|
}
|
|
645
596
|
}
|
|
646
597
|
|
|
647
|
-
// Default
|
|
598
|
+
// Default format
|
|
648
599
|
return {
|
|
649
|
-
message: error.message || '
|
|
600
|
+
message: response?.message || error.message || 'Unknown error',
|
|
650
601
|
status: error.response?.status || 500,
|
|
651
602
|
details: error
|
|
652
603
|
}
|
|
@@ -654,245 +605,180 @@ app.use(createApi({
|
|
|
654
605
|
}))
|
|
655
606
|
```
|
|
656
607
|
|
|
657
|
-
#### Using Errors in Components
|
|
658
|
-
|
|
659
|
-
```vue
|
|
660
|
-
<script setup lang="ts">
|
|
661
|
-
const { data, error, execute } = useApi('/users', {
|
|
662
|
-
skipErrorNotification: true // Skip global error handler
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
// Access structured error
|
|
666
|
-
if (error.value) {
|
|
667
|
-
console.log(error.value.message) // User-friendly message
|
|
668
|
-
console.log(error.value.status) // HTTP status code
|
|
669
|
-
console.log(error.value.code) // Custom error code
|
|
670
|
-
console.log(error.value.errors) // Validation errors
|
|
671
|
-
}
|
|
672
|
-
</script>
|
|
673
|
-
|
|
674
|
-
<template>
|
|
675
|
-
<div v-if="error" class="error-banner">
|
|
676
|
-
{{ error.message }}
|
|
677
|
-
|
|
678
|
-
<!-- Display validation errors -->
|
|
679
|
-
<ul v-if="error.errors">
|
|
680
|
-
<li v-for="(msgs, field) in error.errors" :key="field">
|
|
681
|
-
<strong>{{ field }}:</strong> {{ msgs.join(', ') }}
|
|
682
|
-
</li>
|
|
683
|
-
</ul>
|
|
684
|
-
</div>
|
|
685
|
-
</template>
|
|
686
|
-
```
|
|
687
|
-
|
|
688
608
|
---
|
|
689
609
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
#### How Token Refresh Works
|
|
610
|
+
## ๐ Authentication & Token Management
|
|
693
611
|
|
|
694
|
-
|
|
612
|
+
> **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
|
|
695
613
|
|
|
696
|
-
|
|
697
|
-
1. Request fails with 401 Unauthorized
|
|
698
|
-
โ
|
|
699
|
-
2. Add request to queue & pause all new requests
|
|
700
|
-
โ
|
|
701
|
-
3. Attempt token refresh (POST to refreshUrl)
|
|
702
|
-
โ
|
|
703
|
-
4. Success? โ Replay all queued requests with new token
|
|
704
|
-
Failure? โ Call onTokenRefreshFailed() & reject queue
|
|
705
|
-
```
|
|
614
|
+
### Basic Auth Setup
|
|
706
615
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
#### Configuration
|
|
616
|
+
Add authentication to your API client:
|
|
710
617
|
|
|
711
618
|
```typescript
|
|
712
619
|
const api = createApiClient({
|
|
713
620
|
baseURL: 'https://api.example.com',
|
|
714
|
-
withAuth: true,
|
|
621
|
+
withAuth: true, // Enable automatic token injection
|
|
715
622
|
authOptions: {
|
|
716
|
-
refreshUrl: '/auth/refresh',
|
|
717
|
-
|
|
718
|
-
// โจ NEW: Handle successful refresh response
|
|
719
|
-
onTokenRefreshed: (response) => {
|
|
720
|
-
// Extract additional data from refresh response
|
|
721
|
-
const { user, permissions } = response.data
|
|
722
|
-
|
|
723
|
-
// Update app state
|
|
724
|
-
store.commit('SET_USER', user)
|
|
725
|
-
store.commit('SET_PERMISSIONS', permissions)
|
|
726
|
-
|
|
727
|
-
// Analytics
|
|
728
|
-
analytics.track('token_refreshed', { userId: user.id })
|
|
729
|
-
},
|
|
730
|
-
|
|
623
|
+
refreshUrl: '/auth/refresh',
|
|
731
624
|
onTokenRefreshFailed: () => {
|
|
732
|
-
//
|
|
733
|
-
localStorage.clear()
|
|
625
|
+
// Redirect to login when refresh fails
|
|
734
626
|
window.location.href = '/login'
|
|
735
627
|
}
|
|
736
628
|
}
|
|
737
629
|
})
|
|
738
630
|
```
|
|
739
631
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
// utils/auth.ts
|
|
746
|
-
export const getAccessToken = (): string | null => {
|
|
747
|
-
return localStorage.getItem('access_token')
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export const setAccessToken = (token: string): void => {
|
|
751
|
-
localStorage.setItem('access_token', token)
|
|
752
|
-
}
|
|
632
|
+
The library automatically:
|
|
633
|
+
- Injects `Authorization: Bearer <token>` header
|
|
634
|
+
- Refreshes expired tokens
|
|
635
|
+
- Queues requests during token refresh
|
|
636
|
+
- Retries failed requests after refresh
|
|
753
637
|
|
|
754
|
-
|
|
755
|
-
localStorage.removeItem('access_token')
|
|
756
|
-
}
|
|
757
|
-
```
|
|
638
|
+
---
|
|
758
639
|
|
|
759
|
-
|
|
640
|
+
### Token Management Modes
|
|
760
641
|
|
|
761
|
-
####
|
|
642
|
+
#### Mode 1: localStorage (Default)
|
|
762
643
|
|
|
763
|
-
|
|
644
|
+
Simple setup for development or internal tools:
|
|
764
645
|
|
|
765
646
|
```typescript
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
647
|
+
const api = createApiClient({
|
|
648
|
+
baseURL: 'https://api.example.com',
|
|
649
|
+
authOptions: {
|
|
650
|
+
refreshUrl: '/auth/refresh',
|
|
651
|
+
onTokenRefreshFailed: () => router.push('/login')
|
|
652
|
+
}
|
|
770
653
|
})
|
|
771
654
|
```
|
|
772
655
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
```typescript
|
|
778
|
-
import { setAuthMonitor } from '@ametie/vue-muza-use'
|
|
779
|
-
|
|
780
|
-
setAuthMonitor((event, payload) => {
|
|
781
|
-
console.log(`[Auth Event] ${event}`, payload)
|
|
782
|
-
|
|
783
|
-
// Events: 'token_refresh_start', 'token_refresh_success',
|
|
784
|
-
// 'token_refresh_failed', 'token_injected'
|
|
785
|
-
})
|
|
786
|
-
```
|
|
656
|
+
**Storage:** Both `accessToken` and `refreshToken` in localStorage
|
|
657
|
+
**Security:** โ ๏ธ Vulnerable to XSS attacks
|
|
658
|
+
**Use case:** Development, internal tools
|
|
787
659
|
|
|
788
660
|
---
|
|
789
661
|
|
|
790
|
-
|
|
662
|
+
#### Mode 2: httpOnly Cookies (Production)
|
|
791
663
|
|
|
792
|
-
|
|
664
|
+
Recommended for production apps with sensitive data:
|
|
793
665
|
|
|
794
666
|
```typescript
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
console.log('Success!', response.data)
|
|
802
|
-
analytics.track('api_success', { endpoint: '/analytics' })
|
|
803
|
-
},
|
|
804
|
-
onError: (error) => {
|
|
805
|
-
console.error('Failed:', error.message)
|
|
806
|
-
sentry.captureException(error)
|
|
807
|
-
},
|
|
808
|
-
onFinish: () => {
|
|
809
|
-
console.log('Request finished (success or error)')
|
|
810
|
-
loadingBar.finish()
|
|
667
|
+
const api = createApiClient({
|
|
668
|
+
baseURL: 'https://api.example.com',
|
|
669
|
+
authOptions: {
|
|
670
|
+
refreshUrl: '/auth/refresh',
|
|
671
|
+
refreshWithCredentials: true, // ๐ Send cookies for refresh
|
|
672
|
+
onTokenRefreshFailed: () => router.push('/login')
|
|
811
673
|
}
|
|
812
674
|
})
|
|
813
675
|
```
|
|
814
676
|
|
|
815
|
-
**
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
3. `onSuccess` OR `onError`
|
|
819
|
-
4. `onFinish` (always runs)
|
|
677
|
+
**Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
|
|
678
|
+
**Security:** ๐ Protected from XSS attacks
|
|
679
|
+
**Backend requirement:** Must set `Set-Cookie` with `HttpOnly; Secure; SameSite`
|
|
820
680
|
|
|
821
|
-
|
|
681
|
+
**Common Issues:**
|
|
682
|
+
- **Cookie not sent?** Check cookie domain and `SameSite` attribute
|
|
683
|
+
- **CORS error?** Backend must set `Access-Control-Allow-Credentials: true`
|
|
684
|
+
- **401 on refresh?** Verify cookie is included in request headers
|
|
822
685
|
|
|
823
|
-
|
|
686
|
+
---
|
|
824
687
|
|
|
825
|
-
|
|
688
|
+
### Saving Tokens After Login
|
|
826
689
|
|
|
827
690
|
```typescript
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
691
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
692
|
+
|
|
693
|
+
const { execute } = useApi('/auth/login', {
|
|
694
|
+
method: 'POST',
|
|
695
|
+
authMode: 'public', // No auth for login endpoint
|
|
696
|
+
onSuccess(response) {
|
|
697
|
+
tokenManager.setTokens({
|
|
698
|
+
accessToken: response.data.accessToken,
|
|
699
|
+
refreshToken: response.data.refreshToken, // Optional in cookie mode
|
|
700
|
+
expiresIn: response.data.expiresIn
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
router.push('/dashboard')
|
|
833
704
|
}
|
|
834
705
|
})
|
|
835
706
|
```
|
|
836
707
|
|
|
837
|
-
**When retries happen:**
|
|
838
|
-
- Network errors (timeouts, connection refused)
|
|
839
|
-
- 5xx server errors
|
|
840
|
-
- Configurable via axios retry interceptor
|
|
841
|
-
|
|
842
|
-
**When retries DON'T happen:**
|
|
843
|
-
- 4xx client errors (bad request, unauthorized, etc.)
|
|
844
|
-
- Component is unmounted (automatic cleanup)
|
|
845
|
-
|
|
846
708
|
---
|
|
847
709
|
|
|
848
|
-
###
|
|
710
|
+
### Public Endpoints
|
|
711
|
+
|
|
712
|
+
Skip authentication for public endpoints:
|
|
849
713
|
|
|
850
|
-
#### Local Loading (Per Request)
|
|
851
714
|
```typescript
|
|
852
|
-
|
|
853
|
-
|
|
715
|
+
// Login (no auth needed)
|
|
716
|
+
useApi('/auth/login', {
|
|
717
|
+
method: 'POST',
|
|
718
|
+
authMode: 'public',
|
|
719
|
+
data: credentials
|
|
720
|
+
})
|
|
854
721
|
|
|
855
|
-
//
|
|
722
|
+
// Public blog posts
|
|
723
|
+
useApi('/blog/posts', {
|
|
724
|
+
authMode: 'public',
|
|
725
|
+
immediate: true
|
|
726
|
+
})
|
|
856
727
|
```
|
|
857
728
|
|
|
858
|
-
|
|
859
|
-
```typescript
|
|
860
|
-
const globalLoading = ref(false)
|
|
729
|
+
---
|
|
861
730
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
})
|
|
731
|
+
### Advanced: Custom Refresh Payload
|
|
732
|
+
|
|
733
|
+
Send additional data with token refresh requests:
|
|
866
734
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
735
|
+
```typescript
|
|
736
|
+
const api = createApiClient({
|
|
737
|
+
baseURL: 'https://api.example.com',
|
|
738
|
+
authOptions: {
|
|
739
|
+
refreshUrl: '/auth/refresh',
|
|
740
|
+
|
|
741
|
+
// โ ๏ธ Use function for dynamic data
|
|
742
|
+
refreshPayload: () => ({
|
|
743
|
+
refreshToken: tokenManager.getRefreshToken(),
|
|
744
|
+
deviceId: getDeviceId(),
|
|
745
|
+
timestamp: Date.now()
|
|
746
|
+
})
|
|
747
|
+
}
|
|
870
748
|
})
|
|
871
749
|
```
|
|
872
750
|
|
|
873
|
-
|
|
874
|
-
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
### Advanced: Token Refresh Callback
|
|
754
|
+
|
|
755
|
+
Handle additional data from refresh response:
|
|
875
756
|
|
|
876
757
|
```typescript
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
758
|
+
const api = createApiClient({
|
|
759
|
+
baseURL: 'https://api.example.com',
|
|
760
|
+
authOptions: {
|
|
761
|
+
refreshUrl: '/auth/refresh',
|
|
762
|
+
|
|
763
|
+
// Called after successful token refresh
|
|
764
|
+
onTokenRefreshed: (response) => {
|
|
765
|
+
const { user, permissions } = response.data
|
|
766
|
+
|
|
767
|
+
// Update app state
|
|
768
|
+
store.commit('SET_USER', user)
|
|
769
|
+
store.commit('SET_PERMISSIONS', permissions)
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
onTokenRefreshFailed: () => {
|
|
773
|
+
localStorage.clear()
|
|
774
|
+
window.location.href = '/login'
|
|
775
|
+
}
|
|
892
776
|
}
|
|
893
777
|
})
|
|
894
778
|
```
|
|
895
779
|
|
|
780
|
+
---
|
|
781
|
+
|
|
896
782
|
## ๐ API Reference
|
|
897
783
|
|
|
898
784
|
### `useApi<T, D>(url, options)`
|
|
@@ -983,11 +869,14 @@ The main composable for making HTTP requests.
|
|
|
983
869
|
data: Ref<T | null> // Response data
|
|
984
870
|
loading: Ref<boolean> // Loading state
|
|
985
871
|
error: Ref<ApiError | null> // Error object
|
|
872
|
+
statusCode: Ref<number | null> // HTTP status code
|
|
986
873
|
response: Ref<AxiosResponse<T>> // Full Axios response
|
|
987
874
|
|
|
988
875
|
// Methods
|
|
989
876
|
execute: (config?: AxiosRequestConfig) => Promise<T | null>
|
|
877
|
+
setData: (data: T | null | ((prev: T | null) => T | null)) => void
|
|
990
878
|
abort: (reason?: string) => void
|
|
879
|
+
reset: () => void
|
|
991
880
|
}
|
|
992
881
|
```
|
|
993
882
|
|
|
@@ -1004,6 +893,24 @@ await execute()
|
|
|
1004
893
|
await execute({ params: { page: 2 } })
|
|
1005
894
|
```
|
|
1006
895
|
|
|
896
|
+
#### `setData(newData)`
|
|
897
|
+
Manually update the `data` ref. Supports direct values or updater functions:
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
const { data, setData } = useApi<User[]>('/users')
|
|
901
|
+
|
|
902
|
+
// Direct value
|
|
903
|
+
setData([{ id: 1, name: 'John' }])
|
|
904
|
+
|
|
905
|
+
// Updater function (like React's setState)
|
|
906
|
+
setData(prev => prev ? [...prev, newUser] : [newUser])
|
|
907
|
+
|
|
908
|
+
// Remove item
|
|
909
|
+
setData(prev => prev?.filter(u => u.id !== userId) ?? null)
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
> **Note:** `setData` automatically clears any existing error.
|
|
913
|
+
|
|
1007
914
|
#### `abort(reason?)`
|
|
1008
915
|
Cancel the current request:
|
|
1009
916
|
|
|
@@ -1016,6 +923,20 @@ execute()
|
|
|
1016
923
|
setTimeout(() => abort('Timeout'), 5000)
|
|
1017
924
|
```
|
|
1018
925
|
|
|
926
|
+
#### `reset()`
|
|
927
|
+
Reset all state to initial values:
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
const { data, error, loading, reset } = useApi('/users')
|
|
931
|
+
|
|
932
|
+
// Clear everything
|
|
933
|
+
reset()
|
|
934
|
+
// data.value = null, error.value = null, loading.value = false
|
|
935
|
+
|
|
936
|
+
// Cancel after 5 seconds
|
|
937
|
+
setTimeout(() => abort('Timeout'), 5000)
|
|
938
|
+
```
|
|
939
|
+
|
|
1019
940
|
---
|
|
1020
941
|
|
|
1021
942
|
### `createApiClient(options)`
|