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 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 api management systems**.
10
-
6
+ **Designed for API management systems**.
11
7
 
12
8
  ## ✨ Features
13
9
 
14
- - Optimistic UI (instant update)
15
- - 🧠 Global cache (shared across components)
16
- - ♻️ Auto re-fetch & sync
17
- - 🔐 Optional auth token support
18
- - 🔔 Optional toast / notification support
19
- - 🧩 Zero external state library
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/smartCrudConfig.js`
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://jsonplaceholder.typicode.com",
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
- import "./smartCrudConfig";
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
- ### With optimistic UI
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
- ## 🧠 Manual vs Automatic Notifications
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
- # 🧩 Summary Table (ADD THIS)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-smart-crud",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Minimal optimistic CRUD helper for React without useState, useEffect, or prop drilling",
5
5
  "type": "./src/index.d.ts",
6
6
  "main": "./src/index.js",
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
- const entry = getEntry(url);
8
-
9
- /* ========== OPTIMISTIC ========== */
10
- const optimisticData = options.optimistic ? options.optimistic(data) : data;
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
- // 🔵 Merge server data, overwrite না
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
- // 🟢 Optimistic delete
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
- // 🔴 rollback
124
- entry.data = backup;
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
- // 🟢 body safe parse
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: data.message || "Something went wrong",
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
- return data;
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
@@ -1,7 +1,7 @@
1
- import { config } from "./config";
1
+ import { config } from "./config"
2
2
 
3
3
  export function notify(type, message, meta = {}) {
4
4
  if (typeof config.notify === "function") {
5
- config.notify(type, message, meta);
5
+ config.notify(type, message, meta)
6
6
  }
7
7
  }
package/src/store.js CHANGED
@@ -1,12 +1,12 @@
1
1
  export const store = new Map();
2
2
 
3
3
  export function getEntry(key) {
4
- // console.log("ENTRY KEY:", key);
5
4
  if (!store.has(key)) {
6
5
  store.set(key, {
7
6
  data: [],
8
7
  loading: false,
9
8
  error: null,
9
+ fetched: false,
10
10
  subscribers: new Set(),
11
11
  });
12
12
  }
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 (!entry.loading && entry.data.length === 0) {
14
- entry.loading = true;
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
  }