react-smart-crud 0.1.8 → 0.2.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/README.md +25 -216
- package/package.json +1 -1
- package/src/actions.js +29 -118
- package/src/http.js +19 -10
- package/src/index.js +3 -3
- package/src/normalizeResponse.js +58 -0
- package/src/notify.js +2 -2
- package/src/store.js +1 -1
- package/src/useCrud.js +23 -16
package/README.md
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
|
+
# react-smart-crud
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
# react-smart-crud
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
A **minimal, smart, optimistic CRUD helper for React**
|
|
3
|
+
A **minimal, smart CRUD helper for React**
|
|
7
4
|
No Redux. No Zustand. No boilerplate.
|
|
8
5
|
|
|
9
|
-
**Designed for
|
|
10
|
-
|
|
6
|
+
**Designed for API management systems**.
|
|
11
7
|
|
|
12
8
|
## ✨ Features
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- 🪶 Very small API surface
|
|
10
|
+
* 🧠 Global cache (shared across components)
|
|
11
|
+
* ♻️ Automatic re-fetch & sync after mutations
|
|
12
|
+
* 🔐 Optional auth token support
|
|
13
|
+
* 🔔 Optional toast / notification support
|
|
14
|
+
* 🧩 Zero external state library
|
|
15
|
+
* ✨ Very small API surface
|
|
21
16
|
|
|
22
17
|
---
|
|
23
18
|
|
|
@@ -29,9 +24,9 @@ npm create vite@latest my-project
|
|
|
29
24
|
cd my-project
|
|
30
25
|
|
|
31
26
|
npm install react-smart-crud
|
|
32
|
-
|
|
27
|
+
```
|
|
33
28
|
|
|
34
|
-
Optional dependency:
|
|
29
|
+
Optional dependency for notifications:
|
|
35
30
|
|
|
36
31
|
```bash
|
|
37
32
|
npm install react-hot-toast
|
|
@@ -43,14 +38,14 @@ npm install react-hot-toast
|
|
|
43
38
|
|
|
44
39
|
Create a setup file **once** in your app.
|
|
45
40
|
|
|
46
|
-
### 📄 `src/
|
|
41
|
+
### 📄 `src/main.jsx`
|
|
47
42
|
|
|
48
43
|
```js
|
|
49
44
|
import { setupCrud } from "react-smart-crud";
|
|
50
45
|
import toast from "react-hot-toast";
|
|
51
46
|
|
|
52
47
|
setupCrud({
|
|
53
|
-
baseUrl: "https://
|
|
48
|
+
baseUrl: "https://api.example.com/",
|
|
54
49
|
getToken: () => localStorage.getItem("token"),
|
|
55
50
|
notify: (type, message) => {
|
|
56
51
|
if (type === "success") toast.success(message);
|
|
@@ -58,12 +53,8 @@ setupCrud({
|
|
|
58
53
|
},
|
|
59
54
|
});
|
|
60
55
|
```
|
|
61
|
-
|
|
62
|
-
### 📄 `main.jsx`
|
|
63
|
-
|
|
64
56
|
```js
|
|
65
|
-
|
|
66
|
-
```
|
|
57
|
+
```
|
|
67
58
|
|
|
68
59
|
⚠️ **Do this only once** in your app.
|
|
69
60
|
|
|
@@ -91,19 +82,7 @@ const { data, loading, error } = useCrud("users");
|
|
|
91
82
|
createItem("users", { name: "John" });
|
|
92
83
|
```
|
|
93
84
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```js
|
|
97
|
-
createItem(
|
|
98
|
-
"users",
|
|
99
|
-
{ name: "John" },
|
|
100
|
-
{
|
|
101
|
-
optimistic: (data) => data,
|
|
102
|
-
onSuccess: () => console.log("Created"),
|
|
103
|
-
onError: (err) => console.error(err),
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
```
|
|
85
|
+
> After creation, the library **refetches the data automatically** to keep your cache in sync.
|
|
107
86
|
|
|
108
87
|
---
|
|
109
88
|
|
|
@@ -113,6 +92,8 @@ createItem(
|
|
|
113
92
|
updateItem("users", 1, { name: "Updated" });
|
|
114
93
|
```
|
|
115
94
|
|
|
95
|
+
> After update, all subscribers automatically get the latest data.
|
|
96
|
+
|
|
116
97
|
---
|
|
117
98
|
|
|
118
99
|
## ❌ Delete (DELETE)
|
|
@@ -121,6 +102,8 @@ updateItem("users", 1, { name: "Updated" });
|
|
|
121
102
|
deleteItem("users", 1);
|
|
122
103
|
```
|
|
123
104
|
|
|
105
|
+
> After deletion, cache and UI are automatically updated.
|
|
106
|
+
|
|
124
107
|
---
|
|
125
108
|
|
|
126
109
|
## 📂 Example Endpoints
|
|
@@ -156,9 +139,7 @@ deleteItem("users", 1);
|
|
|
156
139
|
|
|
157
140
|
MIT © Tarequl Islam
|
|
158
141
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
---
|
|
162
143
|
|
|
163
144
|
## ✅ REAL-WORLD EXAMPLE (Vite + React)
|
|
164
145
|
|
|
@@ -201,121 +182,6 @@ export default function UserPage() {
|
|
|
201
182
|
</div>
|
|
202
183
|
);
|
|
203
184
|
}
|
|
204
|
-
````
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## 🔥 Optimistic UI – Full Explanation (ADD THIS)
|
|
210
|
-
|
|
211
|
-
### 🎯 Why Optimistic UI?
|
|
212
|
-
|
|
213
|
-
Optimistic UI means:
|
|
214
|
-
|
|
215
|
-
> **Server response আসার আগেই UI update হবে**
|
|
216
|
-
> Error হলে auto rollback হবে
|
|
217
|
-
|
|
218
|
-
react-smart-crud এ এটা **fully optional**।
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## 🧠 Optimistic Options Structure
|
|
223
|
-
|
|
224
|
-
Every mutation (`createItem`, `updateItem`, `deleteItem`) supports:
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
{
|
|
228
|
-
optimistic?: Function
|
|
229
|
-
onSuccess?: Function
|
|
230
|
-
onError?: Function
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## 🟢 CREATE with Optimistic UI
|
|
237
|
-
|
|
238
|
-
### Example
|
|
239
|
-
|
|
240
|
-
```js
|
|
241
|
-
createItem(
|
|
242
|
-
"users",
|
|
243
|
-
{
|
|
244
|
-
email: form.email,
|
|
245
|
-
password: form.password,
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
// 🔮 optimistic preview data
|
|
249
|
-
optimistic: (data) => ({
|
|
250
|
-
email: data.email,
|
|
251
|
-
role: "user",
|
|
252
|
-
}),
|
|
253
|
-
|
|
254
|
-
onSuccess: () => toast.success("User created"),
|
|
255
|
-
onError: () => toast.error("Failed to create"),
|
|
256
|
-
}
|
|
257
|
-
);
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### How it works
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
1. Temporary item added instantly
|
|
264
|
-
2. `_temp: true` flag attached
|
|
265
|
-
3. Server response merges into same item
|
|
266
|
-
4. Error হলে rollback
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
## 🔄 UPDATE with Optimistic UI (Advanced)
|
|
273
|
-
|
|
274
|
-
### Example
|
|
275
|
-
|
|
276
|
-
```js
|
|
277
|
-
updateItem(
|
|
278
|
-
"users",
|
|
279
|
-
editingUser.id,
|
|
280
|
-
{
|
|
281
|
-
email: form.email,
|
|
282
|
-
role: form.role,
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
optimistic: (old, patch) => ({
|
|
286
|
-
...old,
|
|
287
|
-
email: patch.email,
|
|
288
|
-
role: patch.role,
|
|
289
|
-
}),
|
|
290
|
-
|
|
291
|
-
onSuccess: () => {
|
|
292
|
-
toast.success("Profile updated");
|
|
293
|
-
clearEdit();
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
onError: (err) => toast.error(err.message),
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Optimistic function signature
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
optimistic: (oldItem, newData) => updatedItem
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
✔ You control exactly how UI changes
|
|
308
|
-
✔ Useful for forms, partial updates, toggle switches
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## ❌ DELETE with Manual Error Handling
|
|
313
|
-
|
|
314
|
-
```js
|
|
315
|
-
deleteItem("users", user.id, {
|
|
316
|
-
onSuccess: () => toast.success("Deleted"),
|
|
317
|
-
onError: (err) => toast.error(err.message),
|
|
318
|
-
});
|
|
319
185
|
```
|
|
320
186
|
|
|
321
187
|
---
|
|
@@ -326,11 +192,7 @@ You can use:
|
|
|
326
192
|
|
|
327
193
|
* react-hot-toast
|
|
328
194
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
## 🔧 One-time Setup for Toast
|
|
332
|
-
|
|
333
|
-
### `src/smartCrudConfig.js`
|
|
195
|
+
### One-time Setup for Toast
|
|
334
196
|
|
|
335
197
|
```js
|
|
336
198
|
import { setupCrud } from "react-smart-crud";
|
|
@@ -348,66 +210,13 @@ setupCrud({
|
|
|
348
210
|
|
|
349
211
|
---
|
|
350
212
|
|
|
351
|
-
##
|
|
352
|
-
|
|
353
|
-
### Automatic (inside library)
|
|
354
|
-
|
|
355
|
-
```js
|
|
356
|
-
notify("success", "Deleted");
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### Manual (recommended)
|
|
360
|
-
|
|
361
|
-
```js
|
|
362
|
-
createItem("users", data, {
|
|
363
|
-
onSuccess: () => toast.success("Created"),
|
|
364
|
-
onError: (err) => toast.error(err.message),
|
|
365
|
-
});
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
✔ Full control
|
|
369
|
-
✔ Better UX
|
|
370
|
-
✔ No magic
|
|
371
|
-
|
|
372
|
-
---
|
|
213
|
+
## 💡 Best Practices
|
|
373
214
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
| Action | Optimistic | Rollback | Manual Toast |
|
|
377
|
-
| ---------- | ------------------ | -------- | ------------ |
|
|
378
|
-
| createItem | ✅ | ✅ | ✅ |
|
|
379
|
-
| updateItem | ✅ | ✅ | ✅ |
|
|
380
|
-
| deleteItem | ❌ (instant remove) | ✅ | ✅ |
|
|
381
|
-
|
|
382
|
-
---
|
|
383
|
-
|
|
384
|
-
# 💡 Best Practices (Pro Tips)
|
|
385
|
-
|
|
386
|
-
✔ Always return **full object** from optimistic update
|
|
387
|
-
|
|
388
|
-
✔ Keep optimistic logic **UI-only**
|
|
389
|
-
|
|
390
|
-
✔ Never trust optimistic data as server truth
|
|
215
|
+
✔ Keep mutations simple and rely on **automatic refetch**
|
|
391
216
|
|
|
392
217
|
✔ Handle toast in component, not inside library
|
|
393
218
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
219
|
+
✔ Use `useCrud` in multiple components — all stay in sync
|
|
411
220
|
|
|
412
221
|
---
|
|
413
222
|
|
|
@@ -422,7 +231,7 @@ Global store cache
|
|
|
422
231
|
↓
|
|
423
232
|
API request (once)
|
|
424
233
|
↓
|
|
425
|
-
All subscribers auto update
|
|
234
|
+
All subscribers auto update after mutations
|
|
426
235
|
```
|
|
427
236
|
|
|
428
237
|
👉 Multiple components → **same data, no duplicate fetch**
|
package/package.json
CHANGED
package/src/actions.js
CHANGED
|
@@ -1,138 +1,49 @@
|
|
|
1
1
|
import { getEntry } from "./store";
|
|
2
2
|
import { request } from "./http";
|
|
3
|
-
import { notify } from "./notify";
|
|
4
3
|
|
|
5
4
|
/* ================= CREATE ================= */
|
|
6
|
-
export function createItem(url, data, options = {}) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const tempItem = {
|
|
13
|
-
id: Date.now(),
|
|
14
|
-
...optimisticData,
|
|
15
|
-
_temp: true,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
entry.data = [tempItem, ...entry.data];
|
|
19
|
-
entry.subscribers.forEach((fn) => fn());
|
|
20
|
-
|
|
21
|
-
/* ========== REQUEST ========== */
|
|
22
|
-
request(url, {
|
|
23
|
-
method: "POST",
|
|
24
|
-
headers: { "Content-Type": "application/json" },
|
|
25
|
-
body: JSON.stringify(data),
|
|
26
|
-
})
|
|
27
|
-
.then((serverData) => {
|
|
28
|
-
// ✅ MERGE — replace না
|
|
29
|
-
entry.data = entry.data.map((item) =>
|
|
30
|
-
item._temp ? { ...item, ...serverData, _temp: false } : item
|
|
31
|
-
);
|
|
32
|
-
if (options.onSuccess) {
|
|
33
|
-
options.onSuccess(serverData);
|
|
34
|
-
} else {
|
|
35
|
-
notify("success", "Created", { url, data: serverData });
|
|
36
|
-
}
|
|
37
|
-
options.onSuccess?.(serverData);
|
|
38
|
-
})
|
|
39
|
-
.catch((error) => {
|
|
40
|
-
// 🔴 rollback
|
|
41
|
-
entry.data = entry.data.filter((i) => !i._temp);
|
|
42
|
-
|
|
43
|
-
if (options.onError) {
|
|
44
|
-
options.onError?.(error);
|
|
45
|
-
} else {
|
|
46
|
-
notify("error", error.message || "Create failed", {
|
|
47
|
-
url,
|
|
48
|
-
error,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
.finally(() => {
|
|
53
|
-
entry.subscribers.forEach((fn) => fn());
|
|
5
|
+
export async function createItem(url, data, options = {}) {
|
|
6
|
+
try {
|
|
7
|
+
const serverData = await request(url, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
body: JSON.stringify(data),
|
|
54
10
|
});
|
|
11
|
+
options.onSuccess?.(serverData);
|
|
12
|
+
return serverData;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
options.onError?.(err);
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
55
17
|
}
|
|
18
|
+
|
|
56
19
|
/* ================= UPDATE ================= */
|
|
57
|
-
export function updateItem(url, id, data, options = {}) {
|
|
20
|
+
export async function updateItem(url, id, data, options = {}) {
|
|
58
21
|
const entry = getEntry(url);
|
|
59
|
-
const backup = [...entry.data];
|
|
60
|
-
|
|
61
|
-
// 🟢 OPTIMISTIC UPDATE
|
|
62
|
-
entry.data = entry.data.map((item) =>
|
|
63
|
-
item.id === id
|
|
64
|
-
? {
|
|
65
|
-
...(options.optimistic
|
|
66
|
-
? options.optimistic(item, data)
|
|
67
|
-
: { ...item, ...data }),
|
|
68
|
-
_updating: true,
|
|
69
|
-
}
|
|
70
|
-
: item
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
entry.subscribers.forEach((fn) => fn());
|
|
74
22
|
|
|
75
23
|
request(`${url}/${id}`, {
|
|
76
24
|
method: "PUT",
|
|
77
|
-
headers: { "Content-Type": "application/json" },
|
|
78
25
|
body: JSON.stringify(data),
|
|
79
26
|
})
|
|
80
27
|
.then((serverData) => {
|
|
81
|
-
|
|
82
|
-
entry.data = entry.data.map((item) =>
|
|
83
|
-
item.id === id ? { ...item, ...serverData, _updating: false } : item
|
|
84
|
-
);
|
|
85
|
-
if (options.onSuccess) {
|
|
86
|
-
options.onSuccess(serverData);
|
|
87
|
-
} else {
|
|
88
|
-
notify("success", "Updated", { url, id, data: serverData });
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
.catch((error) => {
|
|
92
|
-
entry.data = backup;
|
|
93
|
-
if (options.onError) {
|
|
94
|
-
options.onError?.(error);
|
|
95
|
-
} else {
|
|
96
|
-
notify("error", error.message || "Update failed", {
|
|
97
|
-
url,
|
|
98
|
-
id,
|
|
99
|
-
error,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
.finally(() => entry.subscribers.forEach((fn) => fn()));
|
|
104
|
-
}
|
|
105
|
-
/* ================= DELETE ================= */
|
|
106
|
-
export function deleteItem(url, id, options = {}) {
|
|
107
|
-
const entry = getEntry(url);
|
|
108
|
-
const backup = [...entry.data];
|
|
28
|
+
entry.fetched = false; // 🔥
|
|
109
29
|
|
|
110
|
-
|
|
111
|
-
entry.data = entry.data.filter((i) => i.id !== id);
|
|
112
|
-
entry.subscribers.forEach((fn) => fn());
|
|
113
|
-
|
|
114
|
-
request(`${url}/${id}`, { method: "DELETE" })
|
|
115
|
-
.then(() => {
|
|
116
|
-
if (options.onSuccess) {
|
|
117
|
-
options.onSuccess();
|
|
118
|
-
} else {
|
|
119
|
-
notify("success", "Deleted", { action: "delete", url, id });
|
|
120
|
-
}
|
|
30
|
+
options.onSuccess?.(serverData);
|
|
121
31
|
})
|
|
122
32
|
.catch((error) => {
|
|
123
|
-
|
|
124
|
-
|
|
33
|
+
options.onError?.(error);
|
|
34
|
+
})
|
|
35
|
+
.finally(() => {
|
|
125
36
|
entry.subscribers.forEach((fn) => fn());
|
|
126
|
-
|
|
127
|
-
if (options.onError) {
|
|
128
|
-
options.onError(error);
|
|
129
|
-
} else {
|
|
130
|
-
notify("error", error.message || "Delete failed", {
|
|
131
|
-
action: "delete",
|
|
132
|
-
url,
|
|
133
|
-
id,
|
|
134
|
-
error,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
37
|
});
|
|
138
38
|
}
|
|
39
|
+
/* ================= DELETE ================= */
|
|
40
|
+
export async function deleteItem(url, id, options = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await request(`${url}/${id}`, { method: "DELETE" });
|
|
43
|
+
options.onSuccess?.(res);
|
|
44
|
+
return res;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
options.onError?.(err);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/http.js
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
import { config } from "./config";
|
|
2
|
+
import { normalizeResponse } from "./normalizeResponse";
|
|
2
3
|
|
|
3
4
|
export async function request(url, options = {}) {
|
|
5
|
+
const method = (options.method || "GET").toUpperCase();
|
|
6
|
+
|
|
4
7
|
const headers = {
|
|
5
8
|
"Content-Type": "application/json",
|
|
6
9
|
...(options.headers || {}),
|
|
7
10
|
};
|
|
8
11
|
|
|
9
|
-
// 🔐 token optional
|
|
10
12
|
if (config.getToken) {
|
|
11
13
|
const token = config.getToken();
|
|
12
|
-
if (token) {
|
|
13
|
-
headers.Authorization = `Bearer ${token}`;
|
|
14
|
-
}
|
|
14
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const res = await fetch(config.baseUrl + url, {
|
|
18
18
|
...options,
|
|
19
|
+
method,
|
|
19
20
|
headers,
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const data = await res.json().catch(() => ({}));
|
|
23
|
+
const json = await res.json().catch(() => ({}));
|
|
24
24
|
|
|
25
|
-
// 🔴 IMPORTANT FIX
|
|
26
25
|
if (!res.ok) {
|
|
27
26
|
throw {
|
|
28
27
|
status: res.status,
|
|
29
|
-
message:
|
|
30
|
-
data,
|
|
28
|
+
message: json.message || "Something went wrong",
|
|
29
|
+
data: json,
|
|
31
30
|
};
|
|
32
31
|
}
|
|
32
|
+
// 🧠 ONLY normalize for GET
|
|
33
|
+
if (method === "GET") {
|
|
34
|
+
try {
|
|
35
|
+
return normalizeResponse(json);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error("Normalize failed", json);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
// POST / PUT / DELETE → raw
|
|
43
|
+
return json;
|
|
35
44
|
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { setupCrud } from "./config"
|
|
2
|
-
export { useCrud } from "./useCrud"
|
|
3
|
-
export { createItem, updateItem, deleteItem } from "./actions"
|
|
1
|
+
export { setupCrud } from "./config"
|
|
2
|
+
export { useCrud } from "./useCrud"
|
|
3
|
+
export { createItem, updateItem, deleteItem } from "./actions"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function isPlainObject(obj) {
|
|
2
|
+
return Object.prototype.toString.call(obj) === "[object Object]";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isDataArray(arr) {
|
|
6
|
+
if (!Array.isArray(arr)) return false;
|
|
7
|
+
if (arr.length === 0) return false; // ⚠️ empty ignore
|
|
8
|
+
|
|
9
|
+
const first = arr[0];
|
|
10
|
+
|
|
11
|
+
// primitive array reject
|
|
12
|
+
if (!isPlainObject(first)) return false;
|
|
13
|
+
|
|
14
|
+
// strong CRUD hint
|
|
15
|
+
if ("id" in first || "_id" in first) return true;
|
|
16
|
+
|
|
17
|
+
// fallback: object array
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeResponse(input) {
|
|
22
|
+
// 1️⃣ Direct array
|
|
23
|
+
if (Array.isArray(input)) return input;
|
|
24
|
+
|
|
25
|
+
if (!isPlainObject(input)) return [];
|
|
26
|
+
|
|
27
|
+
const found = [];
|
|
28
|
+
|
|
29
|
+
function scan(obj, depth = 0) {
|
|
30
|
+
if (depth > 5) return; // 🔒 infinite loop protection
|
|
31
|
+
|
|
32
|
+
for (const value of Object.values(obj)) {
|
|
33
|
+
if (Array.isArray(value) && isDataArray(value)) {
|
|
34
|
+
found.push(value);
|
|
35
|
+
} else if (isPlainObject(value)) {
|
|
36
|
+
scan(value, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
scan(input);
|
|
42
|
+
|
|
43
|
+
// 2️⃣ One clear candidate
|
|
44
|
+
if (found.length === 1) return found[0];
|
|
45
|
+
|
|
46
|
+
// 3️⃣ Multiple → biggest meaningful
|
|
47
|
+
if (found.length > 1) {
|
|
48
|
+
return found.sort((a, b) => b.length - a.length)[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4️⃣ Single object (GET by id)
|
|
52
|
+
if ("id" in input || "_id" in input) {
|
|
53
|
+
return [input];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 5️⃣ Fallback
|
|
57
|
+
return [];
|
|
58
|
+
}
|
package/src/notify.js
CHANGED
package/src/store.js
CHANGED
package/src/useCrud.js
CHANGED
|
@@ -6,34 +6,41 @@ export function useCrud(url) {
|
|
|
6
6
|
const entry = getEntry(url);
|
|
7
7
|
const [, force] = useState(0);
|
|
8
8
|
|
|
9
|
+
const refetch = () => {
|
|
10
|
+
if (entry.loading) return;
|
|
11
|
+
|
|
12
|
+
entry.loading = true;
|
|
13
|
+
entry.subscribers.forEach((fn) => fn());
|
|
14
|
+
|
|
15
|
+
request(url)
|
|
16
|
+
.then((data) => {
|
|
17
|
+
entry.data = data;
|
|
18
|
+
entry.error = null;
|
|
19
|
+
})
|
|
20
|
+
.catch(() => {
|
|
21
|
+
entry.error = "Failed";
|
|
22
|
+
})
|
|
23
|
+
.finally(() => {
|
|
24
|
+
entry.loading = false;
|
|
25
|
+
entry.subscribers.forEach((fn) => fn());
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
9
29
|
useEffect(() => {
|
|
10
30
|
const rerender = () => force((x) => x + 1);
|
|
11
31
|
entry.subscribers.add(rerender);
|
|
12
32
|
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
request(url)
|
|
17
|
-
.then((data) => {
|
|
18
|
-
entry.data = data;
|
|
19
|
-
entry.error = null;
|
|
20
|
-
})
|
|
21
|
-
.catch(() => {
|
|
22
|
-
entry.error = "Failed";
|
|
23
|
-
})
|
|
24
|
-
.finally(() => {
|
|
25
|
-
entry.loading = false;
|
|
26
|
-
entry.subscribers.forEach((fn) => fn());
|
|
27
|
-
});
|
|
33
|
+
if (entry.data.length === 0 && !entry.loading) {
|
|
34
|
+
refetch(); // 🔥 initial fetch
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
return () => entry.subscribers.delete(rerender);
|
|
31
38
|
}, [url]);
|
|
32
39
|
|
|
33
|
-
// 🔑 IMPORTANT: return new reference
|
|
34
40
|
return {
|
|
35
41
|
data: entry.data,
|
|
36
42
|
loading: entry.loading,
|
|
37
43
|
error: entry.error,
|
|
44
|
+
refetch, // ✅ IMPORTANT
|
|
38
45
|
};
|
|
39
46
|
}
|