@kawaiininja/fetch 1.0.54 → 1.0.55
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/hooks/useFetch.js +35 -25
- package/package.json +1 -1
package/dist/hooks/useFetch.js
CHANGED
|
@@ -9,9 +9,28 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
9
9
|
const { fetchCSRF } = useCsrf();
|
|
10
10
|
const abortRef = useRef(new AbortController());
|
|
11
11
|
const mounted = useRef(true);
|
|
12
|
-
const { enabled = true, swr = false, staleTime = 300000, timeout = globalTimeout, retries = globalRetries, } = baseOptions;
|
|
13
|
-
|
|
14
|
-
const
|
|
12
|
+
const { enabled = true, swr = false, staleTime = 300000, timeout = globalTimeout, retries = globalRetries, revalidateOnFocus, refreshInterval, dedupeKey, } = baseOptions;
|
|
13
|
+
// 🛡️ STABILIZE OPTIONS: Prevent identity-based loops by memoizing primitives
|
|
14
|
+
const stableOptions = useMemo(() => ({
|
|
15
|
+
enabled,
|
|
16
|
+
swr,
|
|
17
|
+
staleTime,
|
|
18
|
+
timeout,
|
|
19
|
+
retries,
|
|
20
|
+
revalidateOnFocus,
|
|
21
|
+
refreshInterval,
|
|
22
|
+
dedupeKey,
|
|
23
|
+
}), [
|
|
24
|
+
enabled,
|
|
25
|
+
swr,
|
|
26
|
+
staleTime,
|
|
27
|
+
timeout,
|
|
28
|
+
retries,
|
|
29
|
+
revalidateOnFocus,
|
|
30
|
+
refreshInterval,
|
|
31
|
+
dedupeKey,
|
|
32
|
+
]);
|
|
33
|
+
const cacheKey = useMemo(() => dedupeKey || endpoint, [endpoint, dedupeKey]);
|
|
15
34
|
const cached = getCache(cacheKey);
|
|
16
35
|
const [state, setState] = useState({
|
|
17
36
|
data: cached?.data || null,
|
|
@@ -22,15 +41,12 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
22
41
|
const safeSet = useCallback((fn) => {
|
|
23
42
|
if (!mounted.current)
|
|
24
43
|
return;
|
|
25
|
-
// Immediate update to prevent stale closures and lost updates
|
|
26
44
|
setState((prev) => ({ ...prev, ...fn(prev) }));
|
|
27
45
|
}, []);
|
|
28
46
|
const request = useCallback(async (params = {}) => {
|
|
29
47
|
const isRead = !params.method || params.method === "GET";
|
|
30
48
|
const currentCache = getCache(cacheKey);
|
|
31
49
|
if (isRead && currentCache?.promise) {
|
|
32
|
-
// 🔌 TAP-IN: If a request is already flying (e.g. from StrictMode or another component),
|
|
33
|
-
// we must attach OUR state updater to it, otherwise we'll stay in 'loading: true' forever.
|
|
34
50
|
currentCache.promise.then((data) => safeSet(() => ({ data, status: 200, loading: false })), (err) => safeSet(() => ({
|
|
35
51
|
error: err.message || "Network Error",
|
|
36
52
|
status: null,
|
|
@@ -41,13 +57,13 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
41
57
|
if (!currentCache?.data)
|
|
42
58
|
safeSet(() => ({ loading: true, error: null }));
|
|
43
59
|
const execution = (async () => {
|
|
44
|
-
const reqId = cacheKey;
|
|
60
|
+
const reqId = cacheKey;
|
|
45
61
|
if (debug)
|
|
46
62
|
console.log(`[useFetch] 🚀 Starting ${reqId}`);
|
|
47
63
|
try {
|
|
48
64
|
const token = await fetchCSRF();
|
|
49
65
|
const finalUrl = apiUrl(params.url || endpoint);
|
|
50
|
-
const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...
|
|
66
|
+
const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...stableOptions, ...params }, debug, abortRef.current.signal, apiUrl);
|
|
51
67
|
const parsed = await parseResponse(res, params.parseAs || "auto");
|
|
52
68
|
if (!res.ok)
|
|
53
69
|
throw new Error(parsed?.message || res.statusText);
|
|
@@ -56,7 +72,6 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
56
72
|
if (debug)
|
|
57
73
|
console.log(`[useFetch] ✅ Success ${reqId}`, {
|
|
58
74
|
mounted: mounted.current,
|
|
59
|
-
data: parsed,
|
|
60
75
|
});
|
|
61
76
|
safeSet(() => ({ data: parsed, status: res.status, loading: false }));
|
|
62
77
|
return parsed;
|
|
@@ -71,10 +86,6 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
71
86
|
onError(msg, null);
|
|
72
87
|
throw err;
|
|
73
88
|
}
|
|
74
|
-
else {
|
|
75
|
-
if (debug)
|
|
76
|
-
console.warn(`[useFetch] 🛑 Aborted ${reqId}`);
|
|
77
|
-
}
|
|
78
89
|
}
|
|
79
90
|
finally {
|
|
80
91
|
if (isRead) {
|
|
@@ -98,37 +109,36 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
98
109
|
onError,
|
|
99
110
|
debug,
|
|
100
111
|
safeSet,
|
|
101
|
-
|
|
112
|
+
stableOptions,
|
|
102
113
|
]);
|
|
114
|
+
// 🔄 TRIGGER FETCH
|
|
103
115
|
useEffect(() => {
|
|
104
116
|
if (!enabled)
|
|
105
117
|
return;
|
|
106
|
-
|
|
118
|
+
// Check cache directly inside effect for freshest state without re-triggering dependency
|
|
119
|
+
const currentCache = getCache(cacheKey);
|
|
120
|
+
const shouldFetch = !currentCache?.data || (swr && isStale(cacheKey, staleTime));
|
|
107
121
|
if (shouldFetch) {
|
|
108
122
|
request();
|
|
109
123
|
}
|
|
110
|
-
}, [enabled, cacheKey, swr, staleTime, request
|
|
124
|
+
}, [enabled, cacheKey, swr, staleTime, request]);
|
|
111
125
|
useEffect(() => {
|
|
112
|
-
if (!
|
|
126
|
+
if (!revalidateOnFocus || !enabled)
|
|
113
127
|
return;
|
|
114
128
|
const onFocus = () => request();
|
|
115
129
|
window.addEventListener("focus", onFocus);
|
|
116
130
|
return () => window.removeEventListener("focus", onFocus);
|
|
117
|
-
}, [request,
|
|
131
|
+
}, [request, revalidateOnFocus, enabled]);
|
|
118
132
|
useEffect(() => {
|
|
119
|
-
if (!
|
|
133
|
+
if (!refreshInterval || !enabled)
|
|
120
134
|
return;
|
|
121
|
-
const timer = setInterval(() => request(),
|
|
135
|
+
const timer = setInterval(() => request(), refreshInterval);
|
|
122
136
|
return () => clearInterval(timer);
|
|
123
|
-
}, [request,
|
|
137
|
+
}, [request, refreshInterval, enabled]);
|
|
124
138
|
useEffect(() => {
|
|
125
139
|
mounted.current = true;
|
|
126
140
|
return () => {
|
|
127
141
|
mounted.current = false;
|
|
128
|
-
// 🛡️ STRICT MODE FIX: Do not abort requests on unmount.
|
|
129
|
-
// In React 18 Strict Mode, the first mount starts the request, then unmounts immediately.
|
|
130
|
-
// If we abort here, the shared promise (used by the second mount) dies, leaving the app in 'loading: true'.
|
|
131
|
-
// abortRef.current.abort();
|
|
132
142
|
};
|
|
133
143
|
}, []);
|
|
134
144
|
const methods = createApiMethods({
|