@mlyrmdhnn/use-fetch 1.0.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/dist/http/useFetch.d.ts +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.mjs +237 -0
- package/dist/lib/fetch/cache.d.ts +2 -0
- package/dist/lib/fetch/config.d.ts +20 -0
- package/dist/lib/fetch/core.d.ts +13 -0
- package/dist/lib/fetch/dedup.d.ts +4 -0
- package/dist/lib/fetch/helpers.d.ts +2 -0
- package/dist/lib/fetch/interceptor.d.ts +34 -0
- package/dist/lib/fetch/statusHooks.d.ts +4 -0
- package/dist/lib/fetch/types.d.ts +45 -0
- package/dist/lib/fetch/watcher.d.ts +1 -0
- package/package.json +41 -0
- package/readme.md +548 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HttpOptions, HttpHeaders, RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn } from '../lib/fetch/types';
|
|
2
|
+
import { createInterceptor } from '../lib/fetch/interceptor';
|
|
3
|
+
import { FetchConfigInstance } from '../lib/fetch/config';
|
|
4
|
+
export interface UseFetchStatic {
|
|
5
|
+
baseURL: (url: string) => void;
|
|
6
|
+
headers: (headers: HttpHeaders) => void;
|
|
7
|
+
onError: (status: number, fn: (error: any) => void) => void;
|
|
8
|
+
interceptor: {
|
|
9
|
+
request: {
|
|
10
|
+
use: (fn: RequestInterceptorFn) => number;
|
|
11
|
+
eject: (id: number) => void;
|
|
12
|
+
};
|
|
13
|
+
response: {
|
|
14
|
+
use: (fn: ResponseInterceptorFn) => number;
|
|
15
|
+
eject: (id: number) => void;
|
|
16
|
+
};
|
|
17
|
+
error: {
|
|
18
|
+
use: (fn: ErrorInterceptorFn) => number;
|
|
19
|
+
eject: (id: number) => void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
create: (config: Partial<FetchConfigInstance>) => UseFetchInstance;
|
|
23
|
+
}
|
|
24
|
+
export type UseFetchInstance = {
|
|
25
|
+
<T>(endpoint: string, options?: HttpOptions): ReturnType<typeof buildUseFetch<T>>;
|
|
26
|
+
baseURL: (url: string) => void;
|
|
27
|
+
headers: (headers: HttpHeaders) => void;
|
|
28
|
+
onError: (status: number, fn: (error: any) => void) => void;
|
|
29
|
+
interceptor: UseFetchStatic["interceptor"];
|
|
30
|
+
};
|
|
31
|
+
declare function buildUseFetch<T>(endpoint: string, options?: HttpOptions, instanceConfig?: FetchConfigInstance, instanceInterceptors?: ReturnType<typeof createInterceptor>): {
|
|
32
|
+
data: any;
|
|
33
|
+
error: any;
|
|
34
|
+
pending: any;
|
|
35
|
+
status: any;
|
|
36
|
+
links: any;
|
|
37
|
+
from: any;
|
|
38
|
+
to: any;
|
|
39
|
+
total: any;
|
|
40
|
+
currentPage: any;
|
|
41
|
+
execute: () => Promise<any>;
|
|
42
|
+
refresh: () => Promise<any>;
|
|
43
|
+
clear: () => void;
|
|
44
|
+
abort: () => void;
|
|
45
|
+
};
|
|
46
|
+
declare function useFetch<T>(endpoint: string, options?: HttpOptions): {
|
|
47
|
+
data: any;
|
|
48
|
+
error: any;
|
|
49
|
+
pending: any;
|
|
50
|
+
status: any;
|
|
51
|
+
links: any;
|
|
52
|
+
from: any;
|
|
53
|
+
to: any;
|
|
54
|
+
total: any;
|
|
55
|
+
currentPage: any;
|
|
56
|
+
execute: () => Promise<any>;
|
|
57
|
+
refresh: () => Promise<any>;
|
|
58
|
+
clear: () => void;
|
|
59
|
+
abort: () => void;
|
|
60
|
+
};
|
|
61
|
+
declare const _useFetch: typeof useFetch & UseFetchStatic;
|
|
62
|
+
export { _useFetch as useFetch };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("vue");var t=e=>new Promise(t=>setTimeout(t,e)),n=e=>{let t={};return Object.entries(e||{}).forEach(([e,n])=>{n!=null&&(t[e]=String(n))}),new URLSearchParams(t).toString()},r=e=>{let t=sessionStorage.getItem(e);return t?JSON.parse(t):null},i=(e,t)=>{sessionStorage.setItem(e,JSON.stringify(t))},a={baseURL:`your domain`,headers:{},statusHooks:[]},o=e=>({baseURL:e?.baseURL??`your domain`??``,headers:e?.headers??{},statusHooks:[]}),s={baseURL(e){a.baseURL=e},getBaseURL(){return a.baseURL},headers(e){a.headers={...a.headers,...e}},getHeaders(){return a.headers},onError(e,t){a.statusHooks.push({status:e,fn:t})}},c=[],l=async(e,t)=>{for(let n of c)n.status===e&&await n.fn(t)},u=new Map,d=(e,t,n)=>`${t}:${e}:${JSON.stringify(n)}`,f=e=>u.get(e),p=(e,t)=>{u.set(e,t)},m=e=>{u.delete(e)},h=async({endpoint:e,options:o,controller:s,instanceConfig:c,instanceRequestInterceptors:u,instanceResponseInterceptors:h,instanceErrorInterceptors:g})=>{let _=c??a,{method:v=`GET`,params:y={},payload:b=null,credentials:x=!1,headers:S={},timeout:C=1e4,retry:w=0,retryDelay:T=1e3,cache:E=!1,cacheKey:D,baseURL:O=_.baseURL,dedup:k=!1}=o,A=u??[],j=h??[],M=g??[],N=n(y),P=async()=>{let n=0;for(;n<=w;)try{let t=typeof b==`function`?await b():b,n=t instanceof FormData,o=E&&r(D);if(o)return o;s=new AbortController;let u=setTimeout(()=>s.abort(),C),d={method:v,headers:{Accept:`application/json`,...n?{}:{"Content-Type":`application/json`},"X-Requested-With":`XMLHttpRequest`,...a.headers,..._.headers,...S},credentials:x?`include`:`same-origin`,signal:s.signal,body:v===`GET`?void 0:n?t:JSON.stringify(t)};for(let e of A)d=await e.fn(d);let f=``;try{let t=e.startsWith(`/`)?e.slice(1):e;f=new URL(t,O).toString()}catch{throw Error(`
|
|
2
|
+
[useFetch] Failed to create URL.
|
|
3
|
+
Base URL : ${O}
|
|
4
|
+
Endpoint : ${e}
|
|
5
|
+
Example valid:
|
|
6
|
+
baseURL -> http://localhost:8080/api/
|
|
7
|
+
endpoint -> /users
|
|
8
|
+
`)}let p=await fetch(`${f}${N?`?${N}`:``}`,d);clearTimeout(u);let m=await p.json();for(let e of j)m=await e.fn(m);if(!p.ok){await l(p.status,m);let e=c?.statusHooks??[];for(let t of e)t.status===p.status&&await t.fn(m);throw m}return E&&i(D,m),m}catch(e){if(n++,n<=w){await t(T);continue}let r=e;for(let e of M)r=await e.fn(r);throw r}};if(k&&v===`GET`){let t=d(e,v,y),n=f(t);if(n)return n;let r=P().finally(()=>m(t));return p(t,r),r}return P()},g=0,_=()=>{let e=[],t=[],n=[];return{interceptor:{request:{use(t){let n=++g;return e.push({id:n,fn:t}),n},eject(t){let n=e.findIndex(e=>e.id===t);n!==-1&&e.splice(n,1)}},response:{use(e){let n=++g;return t.push({id:n,fn:e}),n},eject(e){let n=t.findIndex(t=>t.id===e);n!==-1&&t.splice(n,1)}},error:{use(e){let t=++g;return n.push({id:t,fn:e}),t},eject(e){let t=n.findIndex(t=>t.id===e);t!==-1&&n.splice(t,1)}}},requestInterceptors:e,responseInterceptors:t,errorInterceptors:n}},{interceptor:v,requestInterceptors:y,responseInterceptors:b,errorInterceptors:x}=_(),S=(t,n)=>{if(t)if((0,e.isRef)(t)){(0,e.watch)(t,n,{deep:!0});return}else if((0,e.isReactive)(t)){(0,e.watch)(t,n,{deep:!0});return}else (0,e.watch)(()=>t,n,{deep:!0})};function C(t,n={},r,i){let{immediate:a=!0,watchParams:o=!1,pagination:s=!1,pick:c=``,transform:l,cacheKey:u=t,onBeforeRequest:d,onSuccess:f,onError:p,onFinally:m,...g}=n,_=(0,e.ref)(null),v=(0,e.ref)(null),y=(0,e.ref)(!1),b=(0,e.ref)(`idle`),x=(0,e.ref)([]),C=(0,e.ref)(0),w=(0,e.ref)(0),T=(0,e.ref)(0),E=(0,e.ref)(1),D=new AbortController,O=async()=>{try{y.value=!0,b.value=`pending`,v.value=null,d?.(n.payload);let e=await h({endpoint:t,controller:D,options:{...g,endpoint:t,cacheKey:u},instanceConfig:r,instanceRequestInterceptors:i?.requestInterceptors,instanceResponseInterceptors:i?.responseInterceptors,instanceErrorInterceptors:i?.errorInterceptors});if(s)return _.value=e.result.data,x.value=e.result.links,C.value=e.result.from,w.value=e.result.to,T.value=e.result.total,E.value=e.result.current_page,b.value=`success`,f?.(e),e;let a=c?e[c]:e;return l&&(a=l(a)),_.value=a,b.value=`success`,f?.(a),a}catch(e){b.value=`error`,v.value=e,p?.(e)}finally{y.value=!1,m?.()}};return a&&O(),o&&S(n.params,()=>O()),{data:_,error:v,pending:y,status:b,links:x,from:C,to:w,total:T,currentPage:E,execute:O,refresh:()=>O(),clear:()=>{_.value=null,v.value=null,b.value=`idle`},abort:()=>D.abort()}}function w(e,t={}){return C(e,t)}var T=w;T.baseURL=s.baseURL,T.headers=s.headers,T.interceptor=v,T.onError=s.onError,T.create=e=>{let t=o(e),n=_(),r=(e,r={})=>C(e,r,t,n);return r.baseURL=e=>{t.baseURL=e},r.headers=e=>{t.headers={...t.headers,...e}},r.onError=(e,n)=>{t.statusHooks.push({status:e,fn:n})},r.interceptor=n.interceptor,r},exports.useFetch=T;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { isReactive as e, isRef as t, ref as n, watch as r } from "vue";
|
|
2
|
+
//#region lib/fetch/helpers.ts
|
|
3
|
+
var i = (e) => new Promise((t) => setTimeout(t, e)), a = (e) => {
|
|
4
|
+
let t = {};
|
|
5
|
+
return Object.entries(e || {}).forEach(([e, n]) => {
|
|
6
|
+
n != null && (t[e] = String(n));
|
|
7
|
+
}), new URLSearchParams(t).toString();
|
|
8
|
+
}, o = (e) => {
|
|
9
|
+
let t = sessionStorage.getItem(e);
|
|
10
|
+
return t ? JSON.parse(t) : null;
|
|
11
|
+
}, s = (e, t) => {
|
|
12
|
+
sessionStorage.setItem(e, JSON.stringify(t));
|
|
13
|
+
}, c = {
|
|
14
|
+
baseURL: "your domain",
|
|
15
|
+
headers: {},
|
|
16
|
+
statusHooks: []
|
|
17
|
+
}, l = (e) => ({
|
|
18
|
+
baseURL: e?.baseURL ?? "your domain" ?? "",
|
|
19
|
+
headers: e?.headers ?? {},
|
|
20
|
+
statusHooks: []
|
|
21
|
+
}), u = {
|
|
22
|
+
baseURL(e) {
|
|
23
|
+
c.baseURL = e;
|
|
24
|
+
},
|
|
25
|
+
getBaseURL() {
|
|
26
|
+
return c.baseURL;
|
|
27
|
+
},
|
|
28
|
+
headers(e) {
|
|
29
|
+
c.headers = {
|
|
30
|
+
...c.headers,
|
|
31
|
+
...e
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
getHeaders() {
|
|
35
|
+
return c.headers;
|
|
36
|
+
},
|
|
37
|
+
onError(e, t) {
|
|
38
|
+
c.statusHooks.push({
|
|
39
|
+
status: e,
|
|
40
|
+
fn: t
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}, d = [], f = async (e, t) => {
|
|
44
|
+
for (let n of d) n.status === e && await n.fn(t);
|
|
45
|
+
}, p = /* @__PURE__ */ new Map(), m = (e, t, n) => `${t}:${e}:${JSON.stringify(n)}`, h = (e) => p.get(e), g = (e, t) => {
|
|
46
|
+
p.set(e, t);
|
|
47
|
+
}, _ = (e) => {
|
|
48
|
+
p.delete(e);
|
|
49
|
+
}, v = async ({ endpoint: e, options: t, controller: n, instanceConfig: r, instanceRequestInterceptors: l, instanceResponseInterceptors: u, instanceErrorInterceptors: d }) => {
|
|
50
|
+
let p = r ?? c, { method: v = "GET", params: y = {}, payload: b = null, credentials: x = !1, headers: S = {}, timeout: C = 1e4, retry: w = 0, retryDelay: T = 1e3, cache: E = !1, cacheKey: D, baseURL: O = p.baseURL, dedup: k = !1 } = t, A = l ?? [], j = u ?? [], M = d ?? [], N = a(y), P = async () => {
|
|
51
|
+
let t = 0;
|
|
52
|
+
for (; t <= w;) try {
|
|
53
|
+
let t = typeof b == "function" ? await b() : b, i = t instanceof FormData, a = E && o(D);
|
|
54
|
+
if (a) return a;
|
|
55
|
+
n = new AbortController();
|
|
56
|
+
let l = setTimeout(() => n.abort(), C), u = {
|
|
57
|
+
method: v,
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
...i ? {} : { "Content-Type": "application/json" },
|
|
61
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
62
|
+
...c.headers,
|
|
63
|
+
...p.headers,
|
|
64
|
+
...S
|
|
65
|
+
},
|
|
66
|
+
credentials: x ? "include" : "same-origin",
|
|
67
|
+
signal: n.signal,
|
|
68
|
+
body: v === "GET" ? void 0 : i ? t : JSON.stringify(t)
|
|
69
|
+
};
|
|
70
|
+
for (let e of A) u = await e.fn(u);
|
|
71
|
+
let d = "";
|
|
72
|
+
try {
|
|
73
|
+
let t = e.startsWith("/") ? e.slice(1) : e;
|
|
74
|
+
d = new URL(t, O).toString();
|
|
75
|
+
} catch {
|
|
76
|
+
throw Error(`
|
|
77
|
+
[useFetch] Failed to create URL.
|
|
78
|
+
Base URL : ${O}
|
|
79
|
+
Endpoint : ${e}
|
|
80
|
+
Example valid:
|
|
81
|
+
baseURL -> http://localhost:8080/api/
|
|
82
|
+
endpoint -> /users
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
let m = await fetch(`${d}${N ? `?${N}` : ""}`, u);
|
|
86
|
+
clearTimeout(l);
|
|
87
|
+
let h = await m.json();
|
|
88
|
+
for (let e of j) h = await e.fn(h);
|
|
89
|
+
if (!m.ok) {
|
|
90
|
+
await f(m.status, h);
|
|
91
|
+
let e = r?.statusHooks ?? [];
|
|
92
|
+
for (let t of e) t.status === m.status && await t.fn(h);
|
|
93
|
+
throw h;
|
|
94
|
+
}
|
|
95
|
+
return E && s(D, h), h;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (t++, t <= w) {
|
|
98
|
+
await i(T);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
let n = e;
|
|
102
|
+
for (let e of M) n = await e.fn(n);
|
|
103
|
+
throw n;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (k && v === "GET") {
|
|
107
|
+
let t = m(e, v, y), n = h(t);
|
|
108
|
+
if (n) return n;
|
|
109
|
+
let r = P().finally(() => _(t));
|
|
110
|
+
return g(t, r), r;
|
|
111
|
+
}
|
|
112
|
+
return P();
|
|
113
|
+
}, y = 0, b = () => {
|
|
114
|
+
let e = [], t = [], n = [];
|
|
115
|
+
return {
|
|
116
|
+
interceptor: {
|
|
117
|
+
request: {
|
|
118
|
+
use(t) {
|
|
119
|
+
let n = ++y;
|
|
120
|
+
return e.push({
|
|
121
|
+
id: n,
|
|
122
|
+
fn: t
|
|
123
|
+
}), n;
|
|
124
|
+
},
|
|
125
|
+
eject(t) {
|
|
126
|
+
let n = e.findIndex((e) => e.id === t);
|
|
127
|
+
n !== -1 && e.splice(n, 1);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
response: {
|
|
131
|
+
use(e) {
|
|
132
|
+
let n = ++y;
|
|
133
|
+
return t.push({
|
|
134
|
+
id: n,
|
|
135
|
+
fn: e
|
|
136
|
+
}), n;
|
|
137
|
+
},
|
|
138
|
+
eject(e) {
|
|
139
|
+
let n = t.findIndex((t) => t.id === e);
|
|
140
|
+
n !== -1 && t.splice(n, 1);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
error: {
|
|
144
|
+
use(e) {
|
|
145
|
+
let t = ++y;
|
|
146
|
+
return n.push({
|
|
147
|
+
id: t,
|
|
148
|
+
fn: e
|
|
149
|
+
}), t;
|
|
150
|
+
},
|
|
151
|
+
eject(e) {
|
|
152
|
+
let t = n.findIndex((t) => t.id === e);
|
|
153
|
+
t !== -1 && n.splice(t, 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
requestInterceptors: e,
|
|
158
|
+
responseInterceptors: t,
|
|
159
|
+
errorInterceptors: n
|
|
160
|
+
};
|
|
161
|
+
}, { interceptor: x, requestInterceptors: S, responseInterceptors: C, errorInterceptors: w } = b(), T = (n, i) => {
|
|
162
|
+
if (n) if (t(n)) {
|
|
163
|
+
r(n, i, { deep: !0 });
|
|
164
|
+
return;
|
|
165
|
+
} else if (e(n)) {
|
|
166
|
+
r(n, i, { deep: !0 });
|
|
167
|
+
return;
|
|
168
|
+
} else r(() => n, i, { deep: !0 });
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region http/useFetch.ts
|
|
172
|
+
function E(e, t = {}, r, i) {
|
|
173
|
+
let { immediate: a = !0, watchParams: o = !1, pagination: s = !1, pick: c = "", transform: l, cacheKey: u = e, onBeforeRequest: d, onSuccess: f, onError: p, onFinally: m, ...h } = t, g = n(null), _ = n(null), y = n(!1), b = n("idle"), x = n([]), S = n(0), C = n(0), w = n(0), E = n(1), D = new AbortController(), O = async () => {
|
|
174
|
+
try {
|
|
175
|
+
y.value = !0, b.value = "pending", _.value = null, d?.(t.payload);
|
|
176
|
+
let n = await v({
|
|
177
|
+
endpoint: e,
|
|
178
|
+
controller: D,
|
|
179
|
+
options: {
|
|
180
|
+
...h,
|
|
181
|
+
endpoint: e,
|
|
182
|
+
cacheKey: u
|
|
183
|
+
},
|
|
184
|
+
instanceConfig: r,
|
|
185
|
+
instanceRequestInterceptors: i?.requestInterceptors,
|
|
186
|
+
instanceResponseInterceptors: i?.responseInterceptors,
|
|
187
|
+
instanceErrorInterceptors: i?.errorInterceptors
|
|
188
|
+
});
|
|
189
|
+
if (s) return g.value = n.result.data, x.value = n.result.links, S.value = n.result.from, C.value = n.result.to, w.value = n.result.total, E.value = n.result.current_page, b.value = "success", f?.(n), n;
|
|
190
|
+
let a = c ? n[c] : n;
|
|
191
|
+
return l && (a = l(a)), g.value = a, b.value = "success", f?.(a), a;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
b.value = "error", _.value = e, p?.(e);
|
|
194
|
+
} finally {
|
|
195
|
+
y.value = !1, m?.();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
return a && O(), o && T(t.params, () => O()), {
|
|
199
|
+
data: g,
|
|
200
|
+
error: _,
|
|
201
|
+
pending: y,
|
|
202
|
+
status: b,
|
|
203
|
+
links: x,
|
|
204
|
+
from: S,
|
|
205
|
+
to: C,
|
|
206
|
+
total: w,
|
|
207
|
+
currentPage: E,
|
|
208
|
+
execute: O,
|
|
209
|
+
refresh: () => O(),
|
|
210
|
+
clear: () => {
|
|
211
|
+
g.value = null, _.value = null, b.value = "idle";
|
|
212
|
+
},
|
|
213
|
+
abort: () => D.abort()
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function D(e, t = {}) {
|
|
217
|
+
return E(e, t);
|
|
218
|
+
}
|
|
219
|
+
var O = D;
|
|
220
|
+
O.baseURL = u.baseURL, O.headers = u.headers, O.interceptor = x, O.onError = u.onError, O.create = (e) => {
|
|
221
|
+
let t = l(e), n = b(), r = (e, r = {}) => E(e, r, t, n);
|
|
222
|
+
return r.baseURL = (e) => {
|
|
223
|
+
t.baseURL = e;
|
|
224
|
+
}, r.headers = (e) => {
|
|
225
|
+
t.headers = {
|
|
226
|
+
...t.headers,
|
|
227
|
+
...e
|
|
228
|
+
};
|
|
229
|
+
}, r.onError = (e, n) => {
|
|
230
|
+
t.statusHooks.push({
|
|
231
|
+
status: e,
|
|
232
|
+
fn: n
|
|
233
|
+
});
|
|
234
|
+
}, r.interceptor = n.interceptor, r;
|
|
235
|
+
};
|
|
236
|
+
//#endregion
|
|
237
|
+
export { O as useFetch };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { HttpHeaders } from './types';
|
|
2
|
+
type StatusHookFn = (error: any) => void | Promise<void>;
|
|
3
|
+
export interface FetchConfigInstance {
|
|
4
|
+
baseURL: string;
|
|
5
|
+
headers: HttpHeaders;
|
|
6
|
+
statusHooks: {
|
|
7
|
+
status: number;
|
|
8
|
+
fn: StatusHookFn;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export declare const globalConfig: FetchConfigInstance;
|
|
12
|
+
export declare const createConfig: (initial?: Partial<FetchConfigInstance>) => FetchConfigInstance;
|
|
13
|
+
export declare const fetchConfig: {
|
|
14
|
+
baseURL(url: string): void;
|
|
15
|
+
getBaseURL(): string;
|
|
16
|
+
headers(headers: HttpHeaders): void;
|
|
17
|
+
getHeaders(): HttpHeaders;
|
|
18
|
+
onError(status: number, fn: StatusHookFn): void;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FetchConfigInstance } from './config';
|
|
2
|
+
import { InterceptorHandler, RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn } from './types';
|
|
3
|
+
interface FetchCoreParams {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
options: any;
|
|
6
|
+
controller: AbortController;
|
|
7
|
+
instanceConfig?: FetchConfigInstance;
|
|
8
|
+
instanceRequestInterceptors?: InterceptorHandler<RequestInterceptorFn>[];
|
|
9
|
+
instanceResponseInterceptors?: InterceptorHandler<ResponseInterceptorFn>[];
|
|
10
|
+
instanceErrorInterceptors?: InterceptorHandler<ErrorInterceptorFn>[];
|
|
11
|
+
}
|
|
12
|
+
export declare const fetchCore: ({ endpoint, options, controller, instanceConfig, instanceRequestInterceptors, instanceResponseInterceptors, instanceErrorInterceptors, }: FetchCoreParams) => Promise<any>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const buildDedupKey: (endpoint: string, method: string, params: Record<string, any>) => string;
|
|
2
|
+
export declare const getDedupRequest: (key: string) => Promise<any> | undefined;
|
|
3
|
+
export declare const setDedupRequest: (key: string, promise: Promise<any>) => void;
|
|
4
|
+
export declare const deleteDedupRequest: (key: string) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RequestInterceptorFn, ResponseInterceptorFn, ErrorInterceptorFn, InterceptorHandler } from './types';
|
|
2
|
+
export declare const createInterceptor: () => {
|
|
3
|
+
interceptor: {
|
|
4
|
+
request: {
|
|
5
|
+
use(fn: RequestInterceptorFn): number;
|
|
6
|
+
eject(id: number): void;
|
|
7
|
+
};
|
|
8
|
+
response: {
|
|
9
|
+
use(fn: ResponseInterceptorFn): number;
|
|
10
|
+
eject(id: number): void;
|
|
11
|
+
};
|
|
12
|
+
error: {
|
|
13
|
+
use(fn: ErrorInterceptorFn): number;
|
|
14
|
+
eject(id: number): void;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
requestInterceptors: InterceptorHandler<RequestInterceptorFn>[];
|
|
18
|
+
responseInterceptors: InterceptorHandler<ResponseInterceptorFn>[];
|
|
19
|
+
errorInterceptors: InterceptorHandler<ErrorInterceptorFn>[];
|
|
20
|
+
};
|
|
21
|
+
export declare const fetchInterceptor: {
|
|
22
|
+
request: {
|
|
23
|
+
use(fn: RequestInterceptorFn): number;
|
|
24
|
+
eject(id: number): void;
|
|
25
|
+
};
|
|
26
|
+
response: {
|
|
27
|
+
use(fn: ResponseInterceptorFn): number;
|
|
28
|
+
eject(id: number): void;
|
|
29
|
+
};
|
|
30
|
+
error: {
|
|
31
|
+
use(fn: ErrorInterceptorFn): number;
|
|
32
|
+
eject(id: number): void;
|
|
33
|
+
};
|
|
34
|
+
}, requestInterceptors: InterceptorHandler<RequestInterceptorFn>[], responseInterceptors: InterceptorHandler<ResponseInterceptorFn>[], errorInterceptors: InterceptorHandler<ErrorInterceptorFn>[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
2
|
+
export interface HttpHeaders {
|
|
3
|
+
Authorization?: string;
|
|
4
|
+
Accept?: string;
|
|
5
|
+
"Content-Type"?: string;
|
|
6
|
+
"X-CSRF-TOKEN"?: string;
|
|
7
|
+
"X-XSRF-TOKEN"?: string;
|
|
8
|
+
"X-Requested-With"?: string;
|
|
9
|
+
"X-API-KEY"?: string;
|
|
10
|
+
"Accept-Language"?: string;
|
|
11
|
+
"Cache-Control"?: string;
|
|
12
|
+
[key: string]: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface HttpOptions {
|
|
15
|
+
method?: HttpMethod;
|
|
16
|
+
params?: Record<string, any>;
|
|
17
|
+
payload?: any;
|
|
18
|
+
immediate?: boolean;
|
|
19
|
+
pagination?: boolean;
|
|
20
|
+
watchParams?: boolean;
|
|
21
|
+
credentials?: boolean;
|
|
22
|
+
baseURL?: string;
|
|
23
|
+
pick?: string;
|
|
24
|
+
transform?: (data: any) => any;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
cache?: boolean;
|
|
27
|
+
cacheKey?: string;
|
|
28
|
+
retry?: number;
|
|
29
|
+
retryDelay?: number;
|
|
30
|
+
onBeforeRequest?: (payload: any) => void;
|
|
31
|
+
onSuccess?: (data: any) => void;
|
|
32
|
+
onError?: (error: any) => void;
|
|
33
|
+
onFinally?: () => void;
|
|
34
|
+
headers?: HttpHeaders;
|
|
35
|
+
xsrfCookieName?: string;
|
|
36
|
+
xsrfHeaderName?: string;
|
|
37
|
+
dedup?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type RequestInterceptorFn = (config: RequestInit) => RequestInit | Promise<RequestInit>;
|
|
40
|
+
export type ResponseInterceptorFn = (response: any) => any | Promise<any>;
|
|
41
|
+
export type ErrorInterceptorFn = (error: any) => any | Promise<any>;
|
|
42
|
+
export interface InterceptorHandler<T> {
|
|
43
|
+
id: number;
|
|
44
|
+
fn: T;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createWatcher: (params: any, callback: () => void) => void;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mlyrmdhnn/use-fetch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight zero-dependency HTTP composable for Vue 3.5+",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"vue",
|
|
20
|
+
"vue3",
|
|
21
|
+
"composable",
|
|
22
|
+
"fetch",
|
|
23
|
+
"typescript",
|
|
24
|
+
"http"
|
|
25
|
+
],
|
|
26
|
+
"author": "mlyrmdhnn",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"vue": ">=3.5.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "vite build",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.9.1",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vite": "^8.0.14",
|
|
39
|
+
"vite-plugin-dts": "^5.0.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# useFetch
|
|
2
|
+
|
|
3
|
+
A lightweight, modular, zero-dependency HTTP composable for Vue 3.5+, inspired by Nuxt's `useFetch`. Built with TypeScript, works with both **JavaScript** and **TypeScript** Vue projects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Full **TypeScript** support — works with JS too
|
|
10
|
+
- **Modular architecture** — core, cache, interceptors, watcher, dedup all separated
|
|
11
|
+
- **Auto-execute** on mount with `immediate`
|
|
12
|
+
- **Retry** failed requests with configurable delay
|
|
13
|
+
- **Laravel pagination** support out of the box
|
|
14
|
+
- **Reactive params** watcher (`ref`, `reactive`, or plain object)
|
|
15
|
+
- **Session cache** with custom cache key
|
|
16
|
+
- **Timeout** & **AbortController** support
|
|
17
|
+
- **Pick** & **transform** response data
|
|
18
|
+
- **Global interceptors** — request, response, and error with `eject` support
|
|
19
|
+
- **Status hooks** — global HTTP status code error handling
|
|
20
|
+
- **Request deduplication** — prevent duplicate in-flight requests
|
|
21
|
+
- **Multiple instances** — isolated configs per API service
|
|
22
|
+
- **Credentials** / cookie support
|
|
23
|
+
- **FormData** auto-detection
|
|
24
|
+
- Lifecycle callbacks: `onBeforeRequest`, `onSuccess`, `onError`, `onFinally`
|
|
25
|
+
- **Zero dependencies**
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Vue `3.5+`
|
|
32
|
+
- Vite (uses `import.meta.env.VITE_API_URL` as default base URL)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @mlyrmdhnn/usefetch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
Set your base API URL in `.env`:
|
|
47
|
+
|
|
48
|
+
```env
|
|
49
|
+
VITE_API_URL=https://your-api.com/api/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or set it manually in `main.ts` / `main.js`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { useFetch } from "@mlyrmdhnn/usefetch";
|
|
56
|
+
|
|
57
|
+
useFetch.baseURL("https://your-api.com/api/");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Project Structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
lib/
|
|
66
|
+
└── fetch/
|
|
67
|
+
├── cache.ts # SessionStorage cache handler
|
|
68
|
+
├── config.ts # Global config & instance factory
|
|
69
|
+
├── core.ts # Core fetch logic
|
|
70
|
+
├── dedup.ts # Request deduplication
|
|
71
|
+
├── helpers.ts # Utility functions
|
|
72
|
+
├── interceptor.ts # Global & instance interceptors
|
|
73
|
+
├── statusHooks.ts # HTTP status code hooks
|
|
74
|
+
├── types.ts # TypeScript types & interfaces
|
|
75
|
+
└── watcher.ts # Reactive params watcher
|
|
76
|
+
http/
|
|
77
|
+
└── useFetch.ts # Main composable entry point
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Basic Usage
|
|
83
|
+
|
|
84
|
+
### JavaScript
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { useFetch } from "@mlyrmdhnn/usefetch";
|
|
88
|
+
|
|
89
|
+
const { data, pending, error } = useFetch("/users");
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### TypeScript
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { useFetch } from "@mlyrmdhnn/usefetch";
|
|
96
|
+
|
|
97
|
+
const { data, pending, error } = useFetch<User[]>("/users");
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### Signature
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
useFetch<T>(endpoint: string, options?: HttpOptions)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Options
|
|
111
|
+
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
| ----------------- | --------------------- | -------------- | --------------------------------- |
|
|
114
|
+
| `method` | `HttpMethod` | `'GET'` | HTTP method |
|
|
115
|
+
| `params` | `Record<string, any>` | `{}` | URL query parameters |
|
|
116
|
+
| `payload` | `any` | `null` | Request body |
|
|
117
|
+
| `immediate` | `boolean` | `true` | Auto-execute on composable call |
|
|
118
|
+
| `pagination` | `boolean` | `false` | Enable Laravel pagination mode |
|
|
119
|
+
| `watchParams` | `boolean` | `false` | Re-fetch when params change |
|
|
120
|
+
| `credentials` | `boolean` | `false` | Include cookies/credentials |
|
|
121
|
+
| `baseURL` | `string` | `VITE_API_URL` | Override base URL per request |
|
|
122
|
+
| `pick` | `string` | `''` | Pick a specific key from response |
|
|
123
|
+
| `transform` | `(data: any) => any` | - | Transform response data |
|
|
124
|
+
| `timeout` | `number` | `10000` | Request timeout in ms |
|
|
125
|
+
| `cache` | `boolean` | `false` | Enable sessionStorage cache |
|
|
126
|
+
| `cacheKey` | `string` | `endpoint` | Custom cache key |
|
|
127
|
+
| `retry` | `number` | `0` | Number of retry attempts |
|
|
128
|
+
| `retryDelay` | `number` | `1000` | Delay between retries in ms |
|
|
129
|
+
| `dedup` | `boolean` | `false` | Enable request deduplication |
|
|
130
|
+
| `headers` | `HttpHeaders` | `{}` | Custom request headers |
|
|
131
|
+
| `onBeforeRequest` | `(payload) => void` | - | Callback before request fires |
|
|
132
|
+
| `onSuccess` | `(data) => void` | - | Callback on success |
|
|
133
|
+
| `onError` | `(error) => void` | - | Callback on error |
|
|
134
|
+
| `onFinally` | `() => void` | - | Callback after request completes |
|
|
135
|
+
|
|
136
|
+
### Return Values
|
|
137
|
+
|
|
138
|
+
| Value | Type | Description |
|
|
139
|
+
| ------------- | -------------------------------------------------- | --------------------------------- |
|
|
140
|
+
| `data` | `Ref<T \| null>` | Response data |
|
|
141
|
+
| `error` | `Ref<any>` | Error object if request failed |
|
|
142
|
+
| `pending` | `Ref<boolean>` | Loading state |
|
|
143
|
+
| `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status |
|
|
144
|
+
| `links` | `Ref<any[]>` | Pagination links (Laravel) |
|
|
145
|
+
| `from` | `Ref<number>` | Pagination from (Laravel) |
|
|
146
|
+
| `to` | `Ref<number>` | Pagination to (Laravel) |
|
|
147
|
+
| `total` | `Ref<number>` | Total records (Laravel) |
|
|
148
|
+
| `currentPage` | `Ref<number>` | Current page (Laravel) |
|
|
149
|
+
| `execute` | `() => Promise` | Manually trigger request |
|
|
150
|
+
| `refresh` | `() => Promise` | Alias for execute |
|
|
151
|
+
| `clear` | `() => void` | Reset data, error, status to idle |
|
|
152
|
+
| `abort` | `() => void` | Cancel the active request |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
### GET Request
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const { data, pending, error } = useFetch<Product[]>("/products");
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### POST Request
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const { execute, pending } = useFetch("/products", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
payload: {
|
|
172
|
+
name: "New Product",
|
|
173
|
+
price: 50000,
|
|
174
|
+
},
|
|
175
|
+
immediate: false,
|
|
176
|
+
onSuccess(data) {
|
|
177
|
+
console.log("Created:", data);
|
|
178
|
+
},
|
|
179
|
+
onError(err) {
|
|
180
|
+
console.error("Failed:", err);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await execute();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### PUT / PATCH / DELETE
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// PUT
|
|
193
|
+
const { execute } = useFetch("/products/1", {
|
|
194
|
+
method: "PUT",
|
|
195
|
+
payload: { name: "Updated" },
|
|
196
|
+
immediate: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// PATCH
|
|
200
|
+
const { execute } = useFetch("/products/1", {
|
|
201
|
+
method: "PATCH",
|
|
202
|
+
payload: { price: 60000 },
|
|
203
|
+
immediate: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// DELETE
|
|
207
|
+
const { execute } = useFetch("/products/1", {
|
|
208
|
+
method: "DELETE",
|
|
209
|
+
immediate: false,
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### With Query Params
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const { data } = useFetch("/orders", {
|
|
219
|
+
params: {
|
|
220
|
+
status: "active",
|
|
221
|
+
limit: 10,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
> `null` and `undefined` param values are automatically stripped from the query string.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Reactive Params (Auto Re-fetch)
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { reactive } from "vue";
|
|
234
|
+
|
|
235
|
+
const params = reactive({ page: 1, search: "" });
|
|
236
|
+
|
|
237
|
+
const { data } = useFetch("/users", {
|
|
238
|
+
params,
|
|
239
|
+
watchParams: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// changing params will automatically re-fetch
|
|
243
|
+
params.page = 2;
|
|
244
|
+
params.search = "john";
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Supports `ref`, `reactive`, and plain objects.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### Laravel Pagination
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const { data, total, currentPage, links } = useFetch("/invoices", {
|
|
255
|
+
pagination: true,
|
|
256
|
+
params: { page: 1 },
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Expects this response structure from Laravel:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"result": {
|
|
265
|
+
"data": [],
|
|
266
|
+
"links": [],
|
|
267
|
+
"from": 1,
|
|
268
|
+
"to": 10,
|
|
269
|
+
"total": 100,
|
|
270
|
+
"current_page": 1
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### Pick Specific Key
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// Response: { status: true, result: [...] }
|
|
281
|
+
const { data } = useFetch("/users", {
|
|
282
|
+
pick: "result",
|
|
283
|
+
});
|
|
284
|
+
// data.value → [...]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### Transform Response
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const { data } = useFetch("/users", {
|
|
293
|
+
transform: (res) => res.map((user) => user.name),
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### Upload FormData
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const form = new FormData();
|
|
303
|
+
form.append("file", file);
|
|
304
|
+
form.append("name", "document.pdf");
|
|
305
|
+
|
|
306
|
+
const { execute } = useFetch("/upload", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
payload: form,
|
|
309
|
+
immediate: false,
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
> `Content-Type` is automatically omitted for `FormData` so the browser sets the correct multipart boundary.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Session Cache
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const { data } = useFetch("/config", {
|
|
321
|
+
cache: true,
|
|
322
|
+
cacheKey: "app-config",
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> Cached in `sessionStorage`. Clears when the browser tab is closed.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
### Retry on Failure
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const { data } = useFetch("/unstable-endpoint", {
|
|
334
|
+
retry: 3,
|
|
335
|
+
retryDelay: 2000, // wait 2s between each retry
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### Request Deduplication
|
|
342
|
+
|
|
343
|
+
Prevents the same request from firing multiple times simultaneously. Useful when multiple components mount at the same time and need the same data.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
// ComponentA.vue
|
|
347
|
+
const { data } = useFetch("/user/profile", { dedup: true });
|
|
348
|
+
|
|
349
|
+
// ComponentB.vue — mounts at the same time
|
|
350
|
+
const { data } = useFetch("/user/profile", { dedup: true });
|
|
351
|
+
|
|
352
|
+
// Result: only ONE actual HTTP request fires, both components get the same result ✅
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
> Deduplication only applies to `GET` requests.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### Manual Execute + Abort
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
const { execute, abort } = useFetch("/long-request", {
|
|
363
|
+
immediate: false,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
execute();
|
|
367
|
+
abort(); // cancel anytime
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Global Configuration
|
|
373
|
+
|
|
374
|
+
Set once in `main.ts` or `main.js`, applies to every request automatically.
|
|
375
|
+
|
|
376
|
+
### Base URL
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
useFetch.baseURL("https://api.example.com/api/");
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Default Headers
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
useFetch.headers({
|
|
386
|
+
"X-API-KEY": "your-api-key",
|
|
387
|
+
"Accept-Language": "id",
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
> Per-request headers always override global headers.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Global Interceptors
|
|
396
|
+
|
|
397
|
+
### Request Interceptor
|
|
398
|
+
|
|
399
|
+
Runs before every request. Useful for injecting auth tokens globally.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const id = useFetch.interceptor.request.use(async (config) => {
|
|
403
|
+
const token = localStorage.getItem("token");
|
|
404
|
+
if (token) {
|
|
405
|
+
config.headers["Authorization"] = `Bearer ${token}`;
|
|
406
|
+
}
|
|
407
|
+
return config;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// remove later if needed
|
|
411
|
+
useFetch.interceptor.request.eject(id);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Response Interceptor
|
|
415
|
+
|
|
416
|
+
Runs after every successful response.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const id = useFetch.interceptor.response.use(async (result) => {
|
|
420
|
+
console.log("Response received:", result);
|
|
421
|
+
return result;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
useFetch.interceptor.response.eject(id);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Error Interceptor
|
|
428
|
+
|
|
429
|
+
Runs when a request throws or response is not ok.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
const id = useFetch.interceptor.error.use(async (error) => {
|
|
433
|
+
console.error("Request failed:", error);
|
|
434
|
+
return error;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
useFetch.interceptor.error.eject(id);
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Status Hooks
|
|
443
|
+
|
|
444
|
+
Handle specific HTTP status codes globally — no need to repeat error handling in every component.
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// main.ts / main.js
|
|
448
|
+
useFetch.onError(401, () => {
|
|
449
|
+
router.push("/login");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
useFetch.onError(403, () => {
|
|
453
|
+
toast.error("You are not authorized!");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
useFetch.onError(404, () => {
|
|
457
|
+
toast.error("Resource not found!");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
useFetch.onError(500, () => {
|
|
461
|
+
toast.error("Server error, please try again later");
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Multiple Instances
|
|
468
|
+
|
|
469
|
+
Create isolated instances with their own base URL, headers, interceptors, and status hooks. Perfect for projects that communicate with multiple APIs.
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
// main.ts / main.js
|
|
473
|
+
import { useFetch } from "@mlyrmdhnn/usefetch";
|
|
474
|
+
|
|
475
|
+
// global instance
|
|
476
|
+
useFetch.baseURL("https://api.example.com/api/");
|
|
477
|
+
|
|
478
|
+
// isolated instance for payment service
|
|
479
|
+
export const paymentApi = useFetch.create({
|
|
480
|
+
baseURL: "https://payment.example.com/api/",
|
|
481
|
+
headers: { "X-API-KEY": "payment-key" },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// isolated instance for reporting service
|
|
485
|
+
export const reportApi = useFetch.create({
|
|
486
|
+
baseURL: "https://report.example.com/api/",
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Each instance has its own `baseURL`, `headers`, `interceptor`, and `onError`:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
// instance specific interceptor
|
|
494
|
+
paymentApi.interceptor.request.use(async (config) => {
|
|
495
|
+
config.headers["X-Signature"] = generateSignature();
|
|
496
|
+
return config;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// instance specific status hook
|
|
500
|
+
paymentApi.onError(422, (err) => {
|
|
501
|
+
toast.error("Payment validation failed");
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Then use anywhere:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
// global instance
|
|
509
|
+
const { data } = useFetch("/users");
|
|
510
|
+
|
|
511
|
+
// payment instance
|
|
512
|
+
const { data } = paymentApi("/transactions");
|
|
513
|
+
|
|
514
|
+
// report instance
|
|
515
|
+
const { data } = reportApi("/summary");
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## HTTP Methods
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## TypeScript Support
|
|
529
|
+
|
|
530
|
+
All types are exported and available:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import type {
|
|
534
|
+
HttpOptions,
|
|
535
|
+
HttpHeaders,
|
|
536
|
+
HttpMethod,
|
|
537
|
+
RequestInterceptorFn,
|
|
538
|
+
ResponseInterceptorFn,
|
|
539
|
+
ErrorInterceptorFn,
|
|
540
|
+
InterceptorHandler,
|
|
541
|
+
} from "@mlyrmdhnn/usefetch";
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## License
|
|
547
|
+
|
|
548
|
+
MIT
|