@kawaiininja/fetch 1.0.39 → 1.0.41
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 +116 -96
- package/dist/context/ApiContext.d.ts +1 -1
- package/dist/context/ApiContext.js +9 -5
- package/dist/hooks/useFetch.cache.d.ts +10 -0
- package/dist/hooks/useFetch.cache.js +16 -0
- package/dist/hooks/useFetch.d.ts +10 -1
- package/dist/hooks/useFetch.js +76 -36
- package/dist/hooks/utils.d.ts +2 -14
- package/dist/hooks/utils.js +20 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,17 +4,20 @@ A production-grade, hybrid-native HTTP client designed for the Onyx Framework.
|
|
|
4
4
|
|
|
5
5
|
**Rating: S-Tier Utility 🏆**
|
|
6
6
|
|
|
7
|
-
This package automates enterprise-grade security, CSRF handling, and platform detection (Web vs. Native Mobile), allowing you to focus on building features rather than handling HTTP boilerplate.
|
|
7
|
+
This package automates enterprise-grade security, CSRF handling, and platform detection (Web vs. Native Mobile), allowing you to focus on building features rather than handling HTTP boilerplate. It is optimized for high-performance React applications with built-in caching and retry logic.
|
|
8
8
|
|
|
9
9
|
## 🚀 Features
|
|
10
10
|
|
|
11
11
|
- **Hybrid Intelligence**: Automatically detects if running on Web or Native Mobile (Capacitor).
|
|
12
|
+
- **🛡️ Session Memory Vault**: High-performance token caching for native apps that avoids expensive SecureStorage calls on every request.
|
|
12
13
|
- **Pro Security**:
|
|
13
|
-
- **Native**: Uses `SecureStoragePlugin` for token management.
|
|
14
|
-
- **Web**: Uses `HttpOnly` cookies +
|
|
15
|
-
-
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
14
|
+
- **Native**: Uses `SecureStoragePlugin` (with localStorage fallback) for secure token management.
|
|
15
|
+
- **Web**: Uses `HttpOnly` cookies + Automatic CSRF token rotation.
|
|
16
|
+
- **⚡ Strict Reliability**:
|
|
17
|
+
- **3s Execution Deadline**: Requests that take too long are automatically aborted to prevent UI hangs.
|
|
18
|
+
- **3x Auto-Retries**: Transparently retries failed network requests before surfacing an error.
|
|
19
|
+
- **📦 Caching & SWR**: Built-in support for Stale-While-Revalidate, custom dedupe keys, and focus-based revalidation.
|
|
20
|
+
- **Type-Safe & Efficient**: Optimized for both TypeScript and standard JavaScript.
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
@@ -28,9 +31,13 @@ import { ApiProvider } from "@kawaiininja/fetch";
|
|
|
28
31
|
|
|
29
32
|
const apiConfig = {
|
|
30
33
|
baseUrl: "https://api.myapp.com",
|
|
31
|
-
version: "v1",
|
|
34
|
+
version: "v1", // Appends /v1 to all requests
|
|
35
|
+
debug: true, // Enables detailed logging for network and security flows
|
|
32
36
|
// Optional: Global error handler (e.g., for Toast notifications)
|
|
33
|
-
onError: (msg, status) =>
|
|
37
|
+
onError: (msg, status) => {
|
|
38
|
+
// Trigger global snackbar/toast here
|
|
39
|
+
console.error(`[API Error ${status}]: ${msg}`);
|
|
40
|
+
},
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
export default function App() {
|
|
@@ -44,129 +51,142 @@ export default function App() {
|
|
|
44
51
|
|
|
45
52
|
---
|
|
46
53
|
|
|
47
|
-
## 📖 Usage Patterns
|
|
54
|
+
## 📖 Usage Patterns
|
|
48
55
|
|
|
49
|
-
### 1.
|
|
56
|
+
### 1. Simple Data Fetching (SWR Style)
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
By default, `useFetch` can handle loading states and caching automatically.
|
|
52
59
|
|
|
53
60
|
```jsx
|
|
54
61
|
import { useFetch } from "@kawaiininja/fetch";
|
|
55
62
|
|
|
56
|
-
export const
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
export const Profile = () => {
|
|
64
|
+
const { data, isLoading, error, get } = useFetch("/user/me", {
|
|
65
|
+
swr: true, // Return cached data immediately, then fetch updates
|
|
66
|
+
staleTime: 60000, // Consider data fresh for 1 minute
|
|
67
|
+
revalidateOnFocus: true, // Refetch when user switches tabs back
|
|
68
|
+
});
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
const formData = new FormData(e.target);
|
|
63
|
-
const payload = Object.fromEntries(formData);
|
|
64
|
-
|
|
65
|
-
// ✅ Efficient: helper handles JSON stringify, headers, and loading state
|
|
66
|
-
const user = await post(payload);
|
|
67
|
-
|
|
68
|
-
if (user) {
|
|
69
|
-
console.log("Welcome", user.name);
|
|
70
|
-
// Redirect or update context here
|
|
71
|
-
}
|
|
72
|
-
};
|
|
70
|
+
if (isLoading && !data) return <p>Loading...</p>;
|
|
73
71
|
|
|
74
72
|
return (
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<input name="password" type="password" placeholder="Password" />
|
|
80
|
-
|
|
81
|
-
<button disabled={isLoading}>
|
|
82
|
-
{isLoading ? "Logging in..." : "Log In"}
|
|
83
|
-
</button>
|
|
84
|
-
</form>
|
|
73
|
+
<div>
|
|
74
|
+
<h1>{data?.name}</h1>
|
|
75
|
+
<button onClick={() => get()}>Refresh Profile</button>
|
|
76
|
+
</div>
|
|
85
77
|
);
|
|
86
78
|
};
|
|
87
79
|
```
|
|
88
80
|
|
|
89
|
-
### 2.
|
|
81
|
+
### 2. Actions (POST/PUT/PATCH/DELETE)
|
|
90
82
|
|
|
91
|
-
|
|
83
|
+
The hook returns simple helper methods that handle JSON stringification and loading states for you.
|
|
92
84
|
|
|
93
85
|
```jsx
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
setData((prev) => ({ ...prev, name: newName }));
|
|
102
|
-
|
|
103
|
-
// 2. Send request in background
|
|
104
|
-
// If it fails, the error state will update automatically and revert (if you handle it)
|
|
105
|
-
await patch({ name: newName });
|
|
86
|
+
const LoginForm = () => {
|
|
87
|
+
const { post, isLoading } = useFetch("/auth/login");
|
|
88
|
+
|
|
89
|
+
const onSubmit = async (payload) => {
|
|
90
|
+
// helpers return the parsed data directly
|
|
91
|
+
const user = await post(payload);
|
|
92
|
+
if (user) login(user);
|
|
106
93
|
};
|
|
107
94
|
|
|
108
95
|
return (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</div>
|
|
96
|
+
<button onClick={() => onSubmit({ user: "..." })} disabled={isLoading}>
|
|
97
|
+
{isLoading ? "Logging in..." : "Submit"}
|
|
98
|
+
</button>
|
|
113
99
|
);
|
|
114
100
|
};
|
|
115
101
|
```
|
|
116
102
|
|
|
117
|
-
### 3.
|
|
103
|
+
### 3. Advanced Request Options
|
|
118
104
|
|
|
119
|
-
|
|
105
|
+
You can override defaults or pass extra headers/parameters on a per-call basis. Every method (`get`, `post`, `request`, etc.) accepts an options object.
|
|
120
106
|
|
|
121
107
|
```jsx
|
|
122
|
-
|
|
123
|
-
|
|
108
|
+
const { get, post, upload } = useFetch("/data");
|
|
109
|
+
|
|
110
|
+
// Overriding URL or passing custom headers
|
|
111
|
+
get({
|
|
112
|
+
url: "/custom-endpoint",
|
|
113
|
+
headers: { "X-Custom-Header": "value" },
|
|
114
|
+
timeout: 5000, // Override default 3s timeout
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Handling non-JSON responses
|
|
118
|
+
const textData = await get({ parseAs: "text" });
|
|
119
|
+
|
|
120
|
+
// File Uploads
|
|
121
|
+
const handleUpload = (file) => {
|
|
122
|
+
const fd = new FormData();
|
|
123
|
+
fd.append("file", file);
|
|
124
|
+
upload(fd);
|
|
125
|
+
};
|
|
126
|
+
```
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
// 'get' is the trigger function
|
|
127
|
-
const { data, isLoading, get } = useFetch("/dashboard");
|
|
128
|
+
### 4. Reactive Search (Watcher Pattern)
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
// Trigger the fetch when component mounts
|
|
131
|
-
get();
|
|
132
|
-
}, []); // Empty array ensures it only runs once
|
|
130
|
+
Trigger a fetch whenever a specific dependency changes.
|
|
133
131
|
|
|
134
|
-
|
|
132
|
+
```jsx
|
|
133
|
+
const SearchResult = ({ query }) => {
|
|
134
|
+
const { data, get } = useFetch("/search", { enabled: false });
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (query) get({ url: `/search?q=${query}` });
|
|
138
|
+
}, [query]);
|
|
139
|
+
|
|
140
|
+
return <List items={data} />;
|
|
142
141
|
};
|
|
143
142
|
```
|
|
144
143
|
|
|
145
|
-
|
|
144
|
+
---
|
|
146
145
|
|
|
147
|
-
|
|
146
|
+
## ⚙️ Configuration Reference
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
import { useEffect, useState } from "react";
|
|
151
|
-
import { useFetch } from "@kawaiininja/fetch";
|
|
148
|
+
### `useFetch(endpoint, options)`
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
const [filter, setFilter] = useState("all");
|
|
155
|
-
const [search, setSearch] = useState("");
|
|
156
|
-
const { data, get } = useFetch("/items");
|
|
150
|
+
Initial options configured when calling the hook:
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
| Option | Type | Default | Description |
|
|
153
|
+
| :------------------ | :-------- | :--------- | :------------------------------------------------ |
|
|
154
|
+
| `enabled` | `boolean` | `true` | Set to false to prevent automatic fetch on mount. |
|
|
155
|
+
| `swr` | `boolean` | `false` | Enable Stale-While-Revalidate caching. |
|
|
156
|
+
| `staleTime` | `number` | `300000` | MS until cache is considered stale (5 mins). |
|
|
157
|
+
| `revalidateOnFocus` | `boolean` | `false` | Refetch data when window gains focus. |
|
|
158
|
+
| `refreshInterval` | `number` | `0` | Auto-refresh data every X milliseconds. |
|
|
159
|
+
| `dedupeKey` | `string` | `endpoint` | Custom key for caching. |
|
|
163
160
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
### Per-Request Options (passed to `get`, `post`, etc.)
|
|
162
|
+
|
|
163
|
+
| Option | Type | Default | Description |
|
|
164
|
+
| :-------- | :------- | :--------- | :------------------------------------------------------ |
|
|
165
|
+
| `url` | `string` | `endpoint` | Override the default endpoint. |
|
|
166
|
+
| `method` | `string` | `"GET"` | HTTP Method. |
|
|
167
|
+
| `headers` | `object` | `{}` | Custom headers. |
|
|
168
|
+
| `timeout` | `number` | `3000` | Per-request timeout limit. |
|
|
169
|
+
| `retries` | `number` | `3` | Number of times to retry on network failure. |
|
|
170
|
+
| `parseAs` | `string` | `"auto"` | How to parse response (`json`, `text`, `blob`, `auto`). |
|
|
171
|
+
|
|
172
|
+
### Returned Surface
|
|
173
|
+
|
|
174
|
+
- `data`: The parsed response body.
|
|
175
|
+
- `isLoading`: Boolean loading state.
|
|
176
|
+
- `error`: Error message string (if any).
|
|
177
|
+
- `get(options)`, `post(body, options)`, `put(body, options)`, `patch(body, options)`, `del(options)`: HTTP methods.
|
|
178
|
+
- `upload(formData, options)`: Dedicated multipart/form-data helper.
|
|
179
|
+
- `text(options)` / `blob(options)`: Shortcuts for formatted responses.
|
|
180
|
+
- `setData(updater)`: Manually update `data` state.
|
|
181
|
+
- `updateData(partial)`: Merge partial updates into `data`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🛡️ Security Logic
|
|
186
|
+
|
|
187
|
+
1. **Native**: Checks `SecureStorage` -> `localStorage`. If found, injects `Authorization: Bearer <token>` and `X-Session-ID: <id>`. CSRF is skipped.
|
|
188
|
+
2. **Web**: Fetches CSRF token from `/auth/csrf-token` on first boot. Injects `X-CSRF-Token` in all internal requests. Automatically refreshes token on 403 errors.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
© 2024 KawaiiNinja. Built for High-Performance Onyx Apps.
|
|
@@ -16,7 +16,7 @@ export interface ApiContextValue {
|
|
|
16
16
|
debug: boolean;
|
|
17
17
|
}
|
|
18
18
|
export declare const ApiContext: React.Context<ApiContextValue | null>;
|
|
19
|
-
export declare function ApiProvider({ config, children }: {
|
|
19
|
+
export declare function ApiProvider({ config, children, }: {
|
|
20
20
|
config: ApiConfig;
|
|
21
21
|
children: React.ReactNode;
|
|
22
22
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
|
-
export const INTERNAL_HEADER = {
|
|
2
|
+
import { createContext, useCallback, useEffect, useMemo, useRef, } from "react";
|
|
3
|
+
export const INTERNAL_HEADER = {
|
|
4
|
+
"x-internal-service": "zevrinix-main",
|
|
5
|
+
};
|
|
4
6
|
export const ApiContext = createContext(null);
|
|
5
|
-
export function ApiProvider({ config, children }) {
|
|
7
|
+
export function ApiProvider({ config, children, }) {
|
|
6
8
|
const configRef = useRef(config);
|
|
7
9
|
useEffect(() => {
|
|
8
10
|
configRef.current = config;
|
|
9
11
|
}, [config]);
|
|
10
12
|
const apiUrl = useCallback((path) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
if (!path)
|
|
14
|
+
return configRef.current.baseUrl;
|
|
15
|
+
if (path.startsWith("http://") || path.startsWith("https://"))
|
|
13
16
|
return path;
|
|
17
|
+
const base = configRef.current.baseUrl;
|
|
14
18
|
const cleanBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
15
19
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
16
20
|
return `${cleanBase}${cleanPath}`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type CacheEntry = {
|
|
2
|
+
data: any;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
promise?: Promise<any>;
|
|
5
|
+
};
|
|
6
|
+
export declare const getCache: (key: string) => CacheEntry | undefined;
|
|
7
|
+
export declare const setCache: (key: string, data: any, promise?: Promise<any>) => void;
|
|
8
|
+
export declare const isStale: (key: string, staleTime?: number) => boolean;
|
|
9
|
+
export declare const clearCache: () => void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const globalCache = new Map();
|
|
2
|
+
export const getCache = (key) => globalCache.get(key);
|
|
3
|
+
export const setCache = (key, data, promise) => {
|
|
4
|
+
globalCache.set(key, {
|
|
5
|
+
data,
|
|
6
|
+
timestamp: Date.now(),
|
|
7
|
+
promise,
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
export const isStale = (key, staleTime = 300000) => {
|
|
11
|
+
const entry = globalCache.get(key);
|
|
12
|
+
if (!entry)
|
|
13
|
+
return true;
|
|
14
|
+
return Date.now() - entry.timestamp > staleTime;
|
|
15
|
+
};
|
|
16
|
+
export const clearCache = () => globalCache.clear();
|
package/dist/hooks/useFetch.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import { RequestOptions } from "./types";
|
|
1
2
|
import { ApiSurface } from "./utils";
|
|
2
|
-
export
|
|
3
|
+
export interface SSSOptions extends RequestOptions {
|
|
4
|
+
swr?: boolean;
|
|
5
|
+
revalidateOnFocus?: boolean;
|
|
6
|
+
refreshInterval?: number;
|
|
7
|
+
dedupeKey?: string;
|
|
8
|
+
staleTime?: number;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const useFetch: <T = any>(endpoint: string, baseOptions?: SSSOptions) => ApiSurface<T>;
|
package/dist/hooks/useFetch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useApiConfig } from "./useApiConfig";
|
|
3
3
|
import { useCsrf } from "./useCsrf";
|
|
4
|
+
import { getCache, isStale, setCache } from "./useFetch.cache";
|
|
4
5
|
import { parseResponse, performFetch } from "./useFetch.executor";
|
|
5
6
|
import { createApiMethods } from "./utils";
|
|
6
7
|
export const useFetch = (endpoint, baseOptions = {}) => {
|
|
@@ -8,58 +9,97 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
8
9
|
const { fetchCSRF } = useCsrf();
|
|
9
10
|
const abortRef = useRef(new AbortController());
|
|
10
11
|
const mounted = useRef(true);
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
optionsRef.current = baseOptions;
|
|
15
|
-
}, [JSON.stringify(baseOptions)]);
|
|
12
|
+
const { enabled = true, swr = false, staleTime = 300000 } = baseOptions;
|
|
13
|
+
const cacheKey = useMemo(() => baseOptions.dedupeKey || endpoint, [endpoint, baseOptions.dedupeKey]);
|
|
14
|
+
const cached = getCache(cacheKey);
|
|
16
15
|
const [state, setState] = useState({
|
|
17
|
-
data: null,
|
|
18
|
-
loading:
|
|
16
|
+
data: cached?.data || null,
|
|
17
|
+
loading: enabled && !cached?.data,
|
|
19
18
|
error: null,
|
|
20
19
|
status: null,
|
|
21
20
|
});
|
|
22
21
|
const safeSet = useCallback((fn) => {
|
|
23
22
|
if (!mounted.current)
|
|
24
23
|
return;
|
|
25
|
-
// 🚀 STACK FLATTENER: setTimeout(0) physically clears the memory stack
|
|
26
24
|
setTimeout(() => {
|
|
27
25
|
if (mounted.current)
|
|
28
26
|
setState((prev) => ({ ...prev, ...fn(prev) }));
|
|
29
27
|
}, 0);
|
|
30
28
|
}, []);
|
|
31
29
|
const request = useCallback(async (params = {}) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const isRead = !params.method || params.method === "GET";
|
|
31
|
+
const currentCache = getCache(cacheKey);
|
|
32
|
+
if (isRead && currentCache?.promise)
|
|
33
|
+
return currentCache.promise;
|
|
34
|
+
if (!currentCache?.data)
|
|
35
|
+
safeSet(() => ({ loading: true, error: null }));
|
|
36
|
+
const execution = (async () => {
|
|
37
|
+
try {
|
|
38
|
+
const token = await fetchCSRF();
|
|
39
|
+
const finalUrl = apiUrl(params.url || endpoint);
|
|
40
|
+
const res = await performFetch(finalUrl, params.method || "GET", token, params.body, params.headers, { ...baseOptions, ...params }, debug, abortRef.current.signal, apiUrl);
|
|
41
|
+
const parsed = await parseResponse(res, params.parseAs || "auto");
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
throw new Error(parsed?.message || res.statusText);
|
|
44
|
+
if (isRead)
|
|
45
|
+
setCache(cacheKey, parsed);
|
|
46
|
+
safeSet(() => ({ data: parsed, status: res.status, loading: false }));
|
|
47
|
+
return parsed;
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw err;
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err.name !== "AbortError") {
|
|
51
|
+
const msg = err.message || "Network error";
|
|
52
|
+
safeSet(() => ({ error: msg, status: null, loading: false }));
|
|
53
|
+
if (onError)
|
|
54
|
+
onError(msg, null);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (isRead) {
|
|
60
|
+
const entry = getCache(cacheKey);
|
|
61
|
+
if (entry)
|
|
62
|
+
entry.promise = undefined;
|
|
63
|
+
}
|
|
57
64
|
}
|
|
65
|
+
})();
|
|
66
|
+
if (isRead) {
|
|
67
|
+
const entry = getCache(cacheKey);
|
|
68
|
+
if (entry)
|
|
69
|
+
entry.promise = execution;
|
|
58
70
|
}
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
return execution;
|
|
72
|
+
}, [
|
|
73
|
+
endpoint,
|
|
74
|
+
cacheKey,
|
|
75
|
+
fetchCSRF,
|
|
76
|
+
apiUrl,
|
|
77
|
+
onError,
|
|
78
|
+
debug,
|
|
79
|
+
safeSet,
|
|
80
|
+
baseOptions,
|
|
81
|
+
]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!enabled)
|
|
84
|
+
return;
|
|
85
|
+
const shouldFetch = !cached?.data || (swr && isStale(cacheKey, staleTime));
|
|
86
|
+
if (shouldFetch) {
|
|
87
|
+
request();
|
|
61
88
|
}
|
|
62
|
-
}, [
|
|
89
|
+
}, [enabled, cacheKey, swr, staleTime]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!baseOptions.revalidateOnFocus || !enabled)
|
|
92
|
+
return;
|
|
93
|
+
const onFocus = () => request();
|
|
94
|
+
window.addEventListener("focus", onFocus);
|
|
95
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
96
|
+
}, [request, baseOptions.revalidateOnFocus, enabled]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!baseOptions.refreshInterval || !enabled)
|
|
99
|
+
return;
|
|
100
|
+
const timer = setInterval(() => request(), baseOptions.refreshInterval);
|
|
101
|
+
return () => clearInterval(timer);
|
|
102
|
+
}, [request, baseOptions.refreshInterval, enabled]);
|
|
63
103
|
useEffect(() => {
|
|
64
104
|
mounted.current = true;
|
|
65
105
|
return () => {
|
|
@@ -69,7 +109,7 @@ export const useFetch = (endpoint, baseOptions = {}) => {
|
|
|
69
109
|
}, []);
|
|
70
110
|
const methods = useMemo(() => createApiMethods({
|
|
71
111
|
request,
|
|
72
|
-
baseUrl:
|
|
112
|
+
baseUrl: apiUrl(endpoint),
|
|
73
113
|
safeSet,
|
|
74
114
|
}), [request, endpoint, apiUrl, safeSet]);
|
|
75
115
|
return useMemo(() => ({
|
package/dist/hooks/utils.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { FetchState, RequestOptions } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* API surface interface with all request methods
|
|
4
|
-
*/
|
|
5
2
|
export interface ApiSurface<T = any> {
|
|
6
3
|
state: FetchState<T>;
|
|
7
4
|
isLoading: boolean;
|
|
@@ -21,19 +18,13 @@ export interface ApiSurface<T = any> {
|
|
|
21
18
|
blob: (config?: RequestOptions) => Promise<Blob | undefined>;
|
|
22
19
|
upload: <R = T>(formData: FormData, config?: RequestOptions) => Promise<R | undefined>;
|
|
23
20
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Parameters for creating API surface
|
|
26
|
-
*/
|
|
27
21
|
interface CreateApiSurfaceParams<T> {
|
|
28
22
|
state: FetchState<T>;
|
|
29
23
|
request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
|
|
30
24
|
baseUrl: string;
|
|
31
25
|
safeSet: (fn: (prev: FetchState<T>) => Partial<FetchState<T>>) => void;
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
* Helper to construct the STABLE methods of the API surface.
|
|
35
|
-
*/
|
|
36
|
-
export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet }: Omit<CreateApiSurfaceParams<T>, "state">) => {
|
|
27
|
+
export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet, }: Omit<CreateApiSurfaceParams<T>, "state">) => {
|
|
37
28
|
request: <R = T>(options?: RequestOptions) => Promise<R | undefined>;
|
|
38
29
|
refetch: () => Promise<T | undefined>;
|
|
39
30
|
setData: (value: T | null | ((prev: T | null) => T | null)) => void;
|
|
@@ -48,8 +39,5 @@ export declare const createApiMethods: <T = any>({ request, baseUrl, safeSet }:
|
|
|
48
39
|
blob: (config?: RequestOptions) => Promise<Blob | undefined>;
|
|
49
40
|
upload: <R_4 = T>(formData: FormData, config?: RequestOptions) => Promise<R_4 | undefined>;
|
|
50
41
|
};
|
|
51
|
-
|
|
52
|
-
* Helper to construct the API surface object.
|
|
53
|
-
*/
|
|
54
|
-
export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
|
|
42
|
+
export declare const createApiSurface: <T = any>({ state, request, baseUrl, safeSet, }: CreateApiSurfaceParams<T>) => ApiSurface<T>;
|
|
55
43
|
export {};
|
package/dist/hooks/utils.js
CHANGED
|
@@ -1,108 +1,85 @@
|
|
|
1
1
|
import { useApiConfig } from "./useApiConfig";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
export const createApiMethods = ({ request, baseUrl, safeSet }) => {
|
|
6
|
-
// little helper for JSON methods
|
|
2
|
+
export const createApiMethods = ({ request, baseUrl, safeSet, }) => {
|
|
3
|
+
const { apiUrl } = useApiConfig();
|
|
7
4
|
const withJsonBody = (data) => ({
|
|
8
5
|
body: data != null ? JSON.stringify(data) : undefined,
|
|
9
6
|
headers: { "Content-Type": "application/json" },
|
|
10
7
|
parseAs: "json",
|
|
11
8
|
});
|
|
12
|
-
const
|
|
9
|
+
const resolve = (path) => (path ? apiUrl(path) : baseUrl);
|
|
13
10
|
return {
|
|
14
|
-
// 🔁 core
|
|
15
11
|
request,
|
|
16
12
|
refetch: () => request({ url: baseUrl }),
|
|
17
|
-
// 🎯 data helpers
|
|
18
13
|
setData: (value) => {
|
|
19
14
|
safeSet((prev) => ({
|
|
20
|
-
data: (typeof value === "function"
|
|
15
|
+
data: (typeof value === "function"
|
|
16
|
+
? value(prev.data)
|
|
17
|
+
: value),
|
|
21
18
|
}));
|
|
22
19
|
},
|
|
23
20
|
updateData: (partial) => safeSet((prev) => ({
|
|
24
21
|
data: {
|
|
25
22
|
...prev.data,
|
|
26
|
-
...(typeof partial === "function"
|
|
23
|
+
...(typeof partial === "function"
|
|
24
|
+
? partial(prev.data)
|
|
25
|
+
: partial),
|
|
27
26
|
},
|
|
28
27
|
})),
|
|
29
|
-
|
|
30
|
-
get: (config = {}) => request({ ...config, url: config.url || baseUrl, method: "GET" }),
|
|
28
|
+
get: (config = {}) => request({ ...config, url: resolve(config.url), method: "GET" }),
|
|
31
29
|
post: (data, config = {}) => request({
|
|
32
30
|
...config,
|
|
33
|
-
url:
|
|
31
|
+
url: resolve(config.url),
|
|
34
32
|
method: "POST",
|
|
35
33
|
...withJsonBody(data),
|
|
36
|
-
headers: {
|
|
37
|
-
...(config.headers || {}),
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
},
|
|
40
34
|
}),
|
|
41
35
|
put: (data, config = {}) => request({
|
|
42
36
|
...config,
|
|
43
|
-
url: config.url
|
|
37
|
+
url: resolve(config.url),
|
|
44
38
|
method: "PUT",
|
|
45
39
|
...withJsonBody(data),
|
|
46
|
-
headers: {
|
|
47
|
-
...(config.headers || {}),
|
|
48
|
-
"Content-Type": "application/json",
|
|
49
|
-
},
|
|
50
40
|
}),
|
|
51
41
|
patch: (data, config = {}) => request({
|
|
52
42
|
...config,
|
|
53
|
-
url: config.url
|
|
43
|
+
url: resolve(config.url),
|
|
54
44
|
method: "PATCH",
|
|
55
45
|
...withJsonBody(data),
|
|
56
|
-
headers: {
|
|
57
|
-
...(config.headers || {}),
|
|
58
|
-
"Content-Type": "application/json",
|
|
59
|
-
},
|
|
60
46
|
}),
|
|
61
47
|
delete: (config = {}) => request({
|
|
62
48
|
...config,
|
|
63
|
-
url: config.url
|
|
49
|
+
url: resolve(config.url),
|
|
64
50
|
method: "DELETE",
|
|
65
51
|
parseAs: config.parseAs || "json",
|
|
66
52
|
}),
|
|
67
|
-
// 🎭 type-focused helpers
|
|
68
53
|
json: (data, config = {}) => request({
|
|
69
54
|
...config,
|
|
70
|
-
url: config.url
|
|
55
|
+
url: resolve(config.url),
|
|
71
56
|
method: config.method || "POST",
|
|
72
57
|
...withJsonBody(data),
|
|
73
|
-
headers: {
|
|
74
|
-
...(config.headers || {}),
|
|
75
|
-
"Content-Type": "application/json",
|
|
76
|
-
},
|
|
77
58
|
}),
|
|
78
59
|
text: (config = {}) => request({
|
|
79
60
|
...config,
|
|
80
|
-
url: config.url
|
|
61
|
+
url: resolve(config.url),
|
|
81
62
|
method: config.method || "GET",
|
|
82
63
|
parseAs: "text",
|
|
83
64
|
}),
|
|
84
65
|
blob: (config = {}) => request({
|
|
85
66
|
...config,
|
|
86
|
-
url: config.url
|
|
67
|
+
url: resolve(config.url),
|
|
87
68
|
method: config.method || "GET",
|
|
88
69
|
parseAs: "blob",
|
|
89
70
|
}),
|
|
90
71
|
upload: (formData, config = {}) => request({
|
|
91
72
|
...config,
|
|
92
|
-
url: config.url
|
|
93
|
-
method:
|
|
73
|
+
url: resolve(config.url),
|
|
74
|
+
method: "POST",
|
|
94
75
|
body: formData,
|
|
95
76
|
parseAs: config.parseAs || "json",
|
|
96
77
|
}),
|
|
97
78
|
};
|
|
98
79
|
};
|
|
99
|
-
|
|
100
|
-
* Helper to construct the API surface object.
|
|
101
|
-
*/
|
|
102
|
-
export const createApiSurface = ({ state, request, baseUrl, safeSet }) => {
|
|
80
|
+
export const createApiSurface = ({ state, request, baseUrl, safeSet, }) => {
|
|
103
81
|
const methods = createApiMethods({ request, baseUrl, safeSet });
|
|
104
82
|
return {
|
|
105
|
-
// 🔍 state
|
|
106
83
|
state,
|
|
107
84
|
isLoading: state.loading,
|
|
108
85
|
isError: !!state.error,
|