@hookraft/userequest 0.1.1 → 0.1.2

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/index.cjs CHANGED
@@ -81,9 +81,8 @@ function useRequest(key, options = {}) {
81
81
  onStatusChange
82
82
  } = options;
83
83
  const [data, setData] = (0, import_react.useState)(() => {
84
- var _a;
85
84
  if (!key) return void 0;
86
- return (_a = getCached(key, cacheTime)) != null ? _a : void 0;
85
+ return getCached(key, cacheTime) ?? void 0;
87
86
  });
88
87
  const [status, setStatus] = (0, import_react.useState)(() => {
89
88
  if (!key) return "idle";
@@ -111,14 +110,12 @@ function useRequest(key, options = {}) {
111
110
  };
112
111
  }, []);
113
112
  const updateStatus = (0, import_react.useCallback)((next) => {
114
- var _a;
115
113
  if (!mountedRef.current) return;
116
114
  setStatus(next);
117
- (_a = onStatusChangeRef.current) == null ? void 0 : _a.call(onStatusChangeRef, next);
115
+ onStatusChangeRef.current?.(next);
118
116
  }, []);
119
117
  const execute = (0, import_react.useCallback)(
120
118
  async (forceRefresh = false) => {
121
- var _a, _b, _c, _d;
122
119
  if (!key) return;
123
120
  if (!forceRefresh) {
124
121
  const cached = getCached(key, cacheTime);
@@ -141,12 +138,12 @@ function useRequest(key, options = {}) {
141
138
  if (!mountedRef.current) return;
142
139
  setData(result);
143
140
  updateStatus("success");
144
- (_a = onSuccessRef.current) == null ? void 0 : _a.call(onSuccessRef, result);
141
+ onSuccessRef.current?.(result);
145
142
  } catch (err) {
146
143
  if (!mountedRef.current) return;
147
144
  setError(err);
148
145
  updateStatus("error");
149
- (_b = onErrorRef.current) == null ? void 0 : _b.call(onErrorRef, err);
146
+ onErrorRef.current?.(err);
150
147
  }
151
148
  return;
152
149
  }
@@ -164,13 +161,13 @@ function useRequest(key, options = {}) {
164
161
  setData(result);
165
162
  setError(void 0);
166
163
  updateStatus("success");
167
- (_c = onSuccessRef.current) == null ? void 0 : _c.call(onSuccessRef, result);
164
+ onSuccessRef.current?.(result);
168
165
  } catch (err) {
169
166
  clearInFlight(key);
170
167
  if (!mountedRef.current) return;
171
168
  setError(err);
172
169
  updateStatus("error");
173
- (_d = onErrorRef.current) == null ? void 0 : _d.call(onErrorRef, err);
170
+ onErrorRef.current?.(err);
174
171
  }
175
172
  },
176
173
  [key, cacheTime, dedupe, fetcher, updateStatus]
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/useRequest.ts","../src/store.ts"],"sourcesContent":["export { useRequest } from \"./useRequest\"\nexport { clearAll } from \"./store\"\nexport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n CacheEntry,\n} from \"./types\"","import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/useRequest.ts","../src/store.ts"],"sourcesContent":["export { useRequest } from \"./useRequest\"\nexport { clearAll } from \"./store\"\nexport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n CacheEntry,\n} from \"./types\"","import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
@@ -0,0 +1,82 @@
1
+ type RequestStatus = "idle" | "loading" | "success" | "error";
2
+ interface CacheEntry<T> {
3
+ data: T;
4
+ timestamp: number;
5
+ error?: unknown;
6
+ }
7
+ interface UseRequestOptions<T> {
8
+ /**
9
+ * Custom fetcher function. Receives the key and returns a promise.
10
+ * Defaults to a basic fetch + json parse.
11
+ */
12
+ fetcher?: (key: string) => Promise<T>;
13
+ /**
14
+ * How long in ms to keep data in cache before considering it stale.
15
+ * Defaults to 30000 (30 seconds). Set to 0 to disable caching.
16
+ */
17
+ cacheTime?: number;
18
+ /**
19
+ * If true, deduplicates in-flight requests across all components.
20
+ * Defaults to true.
21
+ */
22
+ dedupe?: boolean;
23
+ /**
24
+ * If true, the request will not fire automatically on mount.
25
+ * Call refetch() manually to trigger it.
26
+ */
27
+ manual?: boolean;
28
+ /**
29
+ * Fires when the request succeeds with the response data.
30
+ */
31
+ onSuccess?: (data: T) => void;
32
+ /**
33
+ * Fires when the request fails with the error.
34
+ */
35
+ onError?: (error: unknown) => void;
36
+ /**
37
+ * Fires whenever status changes.
38
+ */
39
+ onStatusChange?: (status: RequestStatus) => void;
40
+ }
41
+ interface UseRequestReturn<T> {
42
+ /** The response data — undefined until request succeeds */
43
+ data: T | undefined;
44
+ /** Current request status */
45
+ status: RequestStatus;
46
+ /** Error if request failed */
47
+ error: unknown;
48
+ /** True while request is in flight */
49
+ isLoading: boolean;
50
+ /** True if request completed successfully */
51
+ isSuccess: boolean;
52
+ /** True if request failed */
53
+ isError: boolean;
54
+ /** Check current status */
55
+ is: (status: RequestStatus) => boolean;
56
+ /** Manually trigger a fresh request — bypasses cache */
57
+ refetch: () => Promise<void>;
58
+ /** Clear the cache for this key */
59
+ clear: () => void;
60
+ }
61
+
62
+ /**
63
+ * useRequest
64
+ *
65
+ * A deduplication-first data fetching hook.
66
+ * Multiple components requesting the same key at the same time
67
+ * will share a single in-flight request — not fire multiple.
68
+ *
69
+ * All caching is in-memory only (JS RAM). Nothing is written
70
+ * to localStorage, sessionStorage, or any browser storage.
71
+ *
72
+ * @example
73
+ * // All three components share ONE network request
74
+ * const { data } = useRequest("/api/user")
75
+ * const { data } = useRequest("/api/user")
76
+ * const { data } = useRequest("/api/user")
77
+ */
78
+ declare function useRequest<T = unknown>(key: string | null, options?: UseRequestOptions<T>): UseRequestReturn<T>;
79
+
80
+ declare function clearAll(): void;
81
+
82
+ export { type CacheEntry, type RequestStatus, type UseRequestOptions, type UseRequestReturn, clearAll, useRequest };
@@ -0,0 +1,82 @@
1
+ type RequestStatus = "idle" | "loading" | "success" | "error";
2
+ interface CacheEntry<T> {
3
+ data: T;
4
+ timestamp: number;
5
+ error?: unknown;
6
+ }
7
+ interface UseRequestOptions<T> {
8
+ /**
9
+ * Custom fetcher function. Receives the key and returns a promise.
10
+ * Defaults to a basic fetch + json parse.
11
+ */
12
+ fetcher?: (key: string) => Promise<T>;
13
+ /**
14
+ * How long in ms to keep data in cache before considering it stale.
15
+ * Defaults to 30000 (30 seconds). Set to 0 to disable caching.
16
+ */
17
+ cacheTime?: number;
18
+ /**
19
+ * If true, deduplicates in-flight requests across all components.
20
+ * Defaults to true.
21
+ */
22
+ dedupe?: boolean;
23
+ /**
24
+ * If true, the request will not fire automatically on mount.
25
+ * Call refetch() manually to trigger it.
26
+ */
27
+ manual?: boolean;
28
+ /**
29
+ * Fires when the request succeeds with the response data.
30
+ */
31
+ onSuccess?: (data: T) => void;
32
+ /**
33
+ * Fires when the request fails with the error.
34
+ */
35
+ onError?: (error: unknown) => void;
36
+ /**
37
+ * Fires whenever status changes.
38
+ */
39
+ onStatusChange?: (status: RequestStatus) => void;
40
+ }
41
+ interface UseRequestReturn<T> {
42
+ /** The response data — undefined until request succeeds */
43
+ data: T | undefined;
44
+ /** Current request status */
45
+ status: RequestStatus;
46
+ /** Error if request failed */
47
+ error: unknown;
48
+ /** True while request is in flight */
49
+ isLoading: boolean;
50
+ /** True if request completed successfully */
51
+ isSuccess: boolean;
52
+ /** True if request failed */
53
+ isError: boolean;
54
+ /** Check current status */
55
+ is: (status: RequestStatus) => boolean;
56
+ /** Manually trigger a fresh request — bypasses cache */
57
+ refetch: () => Promise<void>;
58
+ /** Clear the cache for this key */
59
+ clear: () => void;
60
+ }
61
+
62
+ /**
63
+ * useRequest
64
+ *
65
+ * A deduplication-first data fetching hook.
66
+ * Multiple components requesting the same key at the same time
67
+ * will share a single in-flight request — not fire multiple.
68
+ *
69
+ * All caching is in-memory only (JS RAM). Nothing is written
70
+ * to localStorage, sessionStorage, or any browser storage.
71
+ *
72
+ * @example
73
+ * // All three components share ONE network request
74
+ * const { data } = useRequest("/api/user")
75
+ * const { data } = useRequest("/api/user")
76
+ * const { data } = useRequest("/api/user")
77
+ */
78
+ declare function useRequest<T = unknown>(key: string | null, options?: UseRequestOptions<T>): UseRequestReturn<T>;
79
+
80
+ declare function clearAll(): void;
81
+
82
+ export { type CacheEntry, type RequestStatus, type UseRequestOptions, type UseRequestReturn, clearAll, useRequest };
package/dist/index.js CHANGED
@@ -54,9 +54,8 @@ function useRequest(key, options = {}) {
54
54
  onStatusChange
55
55
  } = options;
56
56
  const [data, setData] = useState(() => {
57
- var _a;
58
57
  if (!key) return void 0;
59
- return (_a = getCached(key, cacheTime)) != null ? _a : void 0;
58
+ return getCached(key, cacheTime) ?? void 0;
60
59
  });
61
60
  const [status, setStatus] = useState(() => {
62
61
  if (!key) return "idle";
@@ -84,14 +83,12 @@ function useRequest(key, options = {}) {
84
83
  };
85
84
  }, []);
86
85
  const updateStatus = useCallback((next) => {
87
- var _a;
88
86
  if (!mountedRef.current) return;
89
87
  setStatus(next);
90
- (_a = onStatusChangeRef.current) == null ? void 0 : _a.call(onStatusChangeRef, next);
88
+ onStatusChangeRef.current?.(next);
91
89
  }, []);
92
90
  const execute = useCallback(
93
91
  async (forceRefresh = false) => {
94
- var _a, _b, _c, _d;
95
92
  if (!key) return;
96
93
  if (!forceRefresh) {
97
94
  const cached = getCached(key, cacheTime);
@@ -114,12 +111,12 @@ function useRequest(key, options = {}) {
114
111
  if (!mountedRef.current) return;
115
112
  setData(result);
116
113
  updateStatus("success");
117
- (_a = onSuccessRef.current) == null ? void 0 : _a.call(onSuccessRef, result);
114
+ onSuccessRef.current?.(result);
118
115
  } catch (err) {
119
116
  if (!mountedRef.current) return;
120
117
  setError(err);
121
118
  updateStatus("error");
122
- (_b = onErrorRef.current) == null ? void 0 : _b.call(onErrorRef, err);
119
+ onErrorRef.current?.(err);
123
120
  }
124
121
  return;
125
122
  }
@@ -137,13 +134,13 @@ function useRequest(key, options = {}) {
137
134
  setData(result);
138
135
  setError(void 0);
139
136
  updateStatus("success");
140
- (_c = onSuccessRef.current) == null ? void 0 : _c.call(onSuccessRef, result);
137
+ onSuccessRef.current?.(result);
141
138
  } catch (err) {
142
139
  clearInFlight(key);
143
140
  if (!mountedRef.current) return;
144
141
  setError(err);
145
142
  updateStatus("error");
146
- (_d = onErrorRef.current) == null ? void 0 : _d.call(onErrorRef, err);
143
+ onErrorRef.current?.(err);
147
144
  }
148
145
  },
149
146
  [key, cacheTime, dedupe, fetcher, updateStatus]
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useRequest.ts","../src/store.ts"],"sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/useRequest.ts","../src/store.ts"],"sourcesContent":["import { useState, useEffect, useCallback, useRef } from \"react\"\nimport {\n getCached,\n setCached,\n clearCached,\n getInFlight,\n setInFlight,\n clearInFlight,\n} from \"./store\"\nimport type {\n RequestStatus,\n UseRequestOptions,\n UseRequestReturn,\n} from \"./types\"\n\nconst defaultFetcher = (key: string) =>\n fetch(key).then((res) => {\n if (!res.ok) throw new Error(`Request failed: ${res.status} ${res.statusText}`)\n return res.json()\n })\n\n/**\n * useRequest\n *\n * A deduplication-first data fetching hook.\n * Multiple components requesting the same key at the same time\n * will share a single in-flight request — not fire multiple.\n *\n * All caching is in-memory only (JS RAM). Nothing is written\n * to localStorage, sessionStorage, or any browser storage.\n *\n * @example\n * // All three components share ONE network request\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n * const { data } = useRequest(\"/api/user\")\n */\nexport function useRequest<T = unknown>(\n key: string | null,\n options: UseRequestOptions<T> = {}\n): UseRequestReturn<T> {\n const {\n fetcher = defaultFetcher as (key: string) => Promise<T>,\n cacheTime = 30_000,\n dedupe = true,\n manual = false,\n onSuccess,\n onError,\n onStatusChange,\n } = options\n\n const [data, setData] = useState<T | undefined>(() => {\n if (!key) return undefined\n return getCached<T>(key, cacheTime) ?? undefined\n })\n const [status, setStatus] = useState<RequestStatus>(() => {\n if (!key) return \"idle\"\n const cached = getCached<T>(key, cacheTime)\n return cached !== null ? \"success\" : \"idle\"\n })\n const [error, setError] = useState<unknown>(undefined)\n\n const mountedRef = useRef(true)\n const onSuccessRef = useRef(onSuccess)\n const onErrorRef = useRef(onError)\n const onStatusChangeRef = useRef(onStatusChange)\n\n // Keep callback refs fresh without causing re-runs\n useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess])\n useEffect(() => { onErrorRef.current = onError }, [onError])\n useEffect(() => { onStatusChangeRef.current = onStatusChange }, [onStatusChange])\n\n useEffect(() => {\n mountedRef.current = true\n return () => { mountedRef.current = false }\n }, [])\n\n const updateStatus = useCallback((next: RequestStatus) => {\n if (!mountedRef.current) return\n setStatus(next)\n onStatusChangeRef.current?.(next)\n }, [])\n\n const execute = useCallback(\n async (forceRefresh = false): Promise<void> => {\n if (!key) return\n\n // Check cache first (unless force refreshing)\n if (!forceRefresh) {\n const cached = getCached<T>(key, cacheTime)\n if (cached !== null) {\n if (mountedRef.current) {\n setData(cached)\n updateStatus(\"success\")\n }\n return\n }\n } else {\n clearCached(key)\n }\n\n // Check for an in-flight request for this key\n if (dedupe) {\n const existing = getInFlight<T>(key)\n if (existing) {\n updateStatus(\"loading\")\n try {\n const result = await existing\n if (!mountedRef.current) return\n setData(result)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n return\n }\n }\n\n // No cache, no in-flight — fire a new request\n updateStatus(\"loading\")\n\n const promise = fetcher(key)\n\n if (dedupe) {\n setInFlight(key, promise)\n }\n\n try {\n const result = await promise\n setCached(key, result)\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setData(result)\n setError(undefined)\n updateStatus(\"success\")\n onSuccessRef.current?.(result)\n } catch (err) {\n clearInFlight(key)\n\n if (!mountedRef.current) return\n setError(err)\n updateStatus(\"error\")\n onErrorRef.current?.(err)\n }\n },\n [key, cacheTime, dedupe, fetcher, updateStatus]\n )\n\n // Auto-fetch on mount unless manual mode\n useEffect(() => {\n if (!key || manual) return\n execute()\n }, [key, manual, execute])\n\n const refetch = useCallback(async () => {\n await execute(true)\n }, [execute])\n\n const clear = useCallback(() => {\n if (!key) return\n clearCached(key)\n setData(undefined)\n setError(undefined)\n updateStatus(\"idle\")\n }, [key, updateStatus])\n\n const is = useCallback(\n (s: RequestStatus) => status === s,\n [status]\n )\n\n return {\n data,\n status,\n error,\n isLoading: status === \"loading\",\n isSuccess: status === \"success\",\n isError: status === \"error\",\n is,\n refetch,\n clear,\n }\n}","import type { CacheEntry, InFlightEntry } from \"./types\"\n\n/**\n * Global in-memory cache — lives in JS RAM only.\n * Never touches localStorage, sessionStorage, or any browser storage.\n * Cleared automatically when the page refreshes or tab closes.\n */\nconst cache = new Map<string, CacheEntry<unknown>>()\n\n/**\n * In-flight registry — tracks requests currently in progress.\n * If a request for the same key is already in flight,\n * new subscribers attach to the existing Promise instead of firing a new request.\n */\nconst inFlight = new Map<string, InFlightEntry<unknown>>()\n\nexport function getCached<T>(key: string, cacheTime: number): T | null {\n const entry = cache.get(key) as CacheEntry<T> | undefined\n if (!entry) return null\n if (cacheTime === 0) return null\n const isStale = Date.now() - entry.timestamp > cacheTime\n if (isStale) {\n cache.delete(key)\n return null\n }\n return entry.data\n}\n\nexport function setCached<T>(key: string, data: T): void {\n cache.set(key, { data, timestamp: Date.now() })\n}\n\nexport function clearCached(key: string): void {\n cache.delete(key)\n}\n\nexport function getInFlight<T>(key: string): Promise<T> | null {\n const entry = inFlight.get(key) as InFlightEntry<T> | undefined\n if (!entry) return null\n entry.subscribers++\n return entry.promise\n}\n\nexport function setInFlight<T>(key: string, promise: Promise<T>): void {\n inFlight.set(key, { promise: promise as Promise<unknown>, subscribers: 1 })\n}\n\nexport function clearInFlight(key: string): void {\n inFlight.delete(key)\n}\n\nexport function clearAll(): void {\n cache.clear()\n inFlight.clear()\n}"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookraft/userequest",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Deduplication-first data fetching hook for React. One request fires no matter how many components ask for the same data at the same time.",
5
5
  "author": "virus",
6
6
  "license": "MIT",