@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.
@@ -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
- const mergedOptions = { ...baseOptions, timeout, retries };
14
- const cacheKey = useMemo(() => baseOptions.dedupeKey || endpoint, [endpoint, baseOptions.dedupeKey]);
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; // Simple ID for logging
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, { ...mergedOptions, ...params }, debug, abortRef.current.signal, apiUrl);
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
- baseOptions,
112
+ stableOptions,
102
113
  ]);
114
+ // 🔄 TRIGGER FETCH
103
115
  useEffect(() => {
104
116
  if (!enabled)
105
117
  return;
106
- const shouldFetch = !cached?.data || (swr && isStale(cacheKey, staleTime));
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, cached?.data]);
124
+ }, [enabled, cacheKey, swr, staleTime, request]);
111
125
  useEffect(() => {
112
- if (!baseOptions.revalidateOnFocus || !enabled)
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, baseOptions.revalidateOnFocus, enabled]);
131
+ }, [request, revalidateOnFocus, enabled]);
118
132
  useEffect(() => {
119
- if (!baseOptions.refreshInterval || !enabled)
133
+ if (!refreshInterval || !enabled)
120
134
  return;
121
- const timer = setInterval(() => request(), baseOptions.refreshInterval);
135
+ const timer = setInterval(() => request(), refreshInterval);
122
136
  return () => clearInterval(timer);
123
- }, [request, baseOptions.refreshInterval, enabled]);
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kawaiininja/fetch",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "description": "Core fetch utility for Onyx Framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",