react-smart-crud 0.1.0 â 0.1.1
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 +295 -253
- package/package.json +15 -12
- package/src/actions.js +114 -0
- package/src/config.js +11 -0
- package/src/http.js +35 -0
- package/src/index.d.ts +32 -0
- package/src/index.js +3 -0
- package/src/notify.js +7 -0
- package/src/store.js +14 -0
- package/src/useCrud.js +39 -0
package/README.md
CHANGED
|
@@ -1,388 +1,430 @@
|
|
|
1
1
|
|
|
2
|
-
# react-smart-crud
|
|
3
|
-
Smart, minimal, and developer-controlled CRUD engine for React â with **Optimistic UI**, **zero prop-drilling**, and **no state management headache**.
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
# react-smart-crud
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
A **minimal, smart, optimistic CRUD helper for React**
|
|
7
|
+
No Redux. No Zustand. No boilerplate.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
**Designed for api management systems**.
|
|
8
10
|
|
|
9
|
-
`react-smart-crud` is a **lightweight state + CRUD utility** designed to remove the most painful parts of React CRUD development:
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
- â No more `useEffect` refetch loops
|
|
13
|
-
- â No prop drilling between components
|
|
14
|
-
- â No forced toast / UI library
|
|
15
|
-
- â No Redux / React Query overhead
|
|
12
|
+
## âĻ Features
|
|
16
13
|
|
|
17
|
-
|
|
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
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
21
|
-
##
|
|
24
|
+
## ðĶ Installation
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Server truth always wins.
|
|
26
|
-
Developer controls UI & UX.**
|
|
26
|
+
```bash
|
|
27
|
+
npm create vite@latest my-project
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
cd my-project
|
|
30
|
+
|
|
31
|
+
npm install react-smart-crud
|
|
32
|
+
````
|
|
33
|
+
|
|
34
|
+
Optional dependency:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install react-hot-toast
|
|
38
|
+
```
|
|
33
39
|
|
|
34
40
|
---
|
|
35
41
|
|
|
36
|
-
##
|
|
42
|
+
## âïļ One-time Setup (Required)
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
- `useState` in parent
|
|
40
|
-
- `useEffect` for fetch
|
|
41
|
-
- Props passed through 3â4 components
|
|
42
|
-
- Re-fetch list after every mutation
|
|
43
|
-
- Toast logic mixed with API logic
|
|
44
|
-
- Server error message lost
|
|
44
|
+
Create a setup file **once** in your app.
|
|
45
45
|
|
|
46
|
-
###
|
|
47
|
-
- â
No `useState` for list data
|
|
48
|
-
- â
No `useEffect` refetch
|
|
49
|
-
- â
No props drilling
|
|
50
|
-
- â
Instant UI update
|
|
51
|
-
- â
Manual toast control
|
|
52
|
-
- â
Real server error shown
|
|
46
|
+
### ð `src/smartCrudConfig.js`
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
```js
|
|
49
|
+
import { setupCrud } from "react-smart-crud";
|
|
50
|
+
import toast from "react-hot-toast";
|
|
51
|
+
|
|
52
|
+
setupCrud({
|
|
53
|
+
baseUrl: "https://jsonplaceholder.typicode.com",
|
|
54
|
+
getToken: () => localStorage.getItem("token"),
|
|
55
|
+
notify: (type, message) => {
|
|
56
|
+
if (type === "success") toast.success(message);
|
|
57
|
+
if (type === "error") toast.error(message);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
### ð `main.jsx`
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
â
Manual toast / notification control
|
|
64
|
-
â
Backend error message preserved
|
|
65
|
-
â
Automatic rollback on failure
|
|
66
|
-
â
REST API friendly
|
|
67
|
-
â
Extremely small & fast
|
|
64
|
+
```js
|
|
65
|
+
import "./smartCrudConfig";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
â ïļ **Do this only once** in your app.
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
|
71
|
-
##
|
|
72
|
+
## ð§ useCrud Hook
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const { data, loading, error } = useCrud("users");
|
|
76
|
+
```
|
|
72
77
|
|
|
73
|
-
###
|
|
74
|
-
- React dashboard projects
|
|
75
|
-
- Admin panels
|
|
76
|
-
- School / ERP / CRM systems
|
|
77
|
-
- MERN stack apps
|
|
78
|
-
- Freelancers & agencies
|
|
79
|
-
- Developers tired of over-engineering
|
|
78
|
+
### Returned values
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
| key | type | description |
|
|
81
|
+
| ------- | ------- | ------------- |
|
|
82
|
+
| data | array | cached data |
|
|
83
|
+
| loading | boolean | request state |
|
|
84
|
+
| error | any | error info |
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## âïļ Create (POST)
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
```js
|
|
91
|
+
createItem("users", { name: "John" });
|
|
92
|
+
```
|
|
93
|
+
|
|
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
|
+
```
|
|
99
107
|
|
|
100
108
|
---
|
|
101
109
|
|
|
102
|
-
##
|
|
110
|
+
## ð Update (PUT)
|
|
103
111
|
|
|
104
|
-
```
|
|
105
|
-
|
|
112
|
+
```js
|
|
113
|
+
updateItem("users", 1, { name: "Updated" });
|
|
106
114
|
```
|
|
107
115
|
|
|
116
|
+
---
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
## â Delete (DELETE)
|
|
110
119
|
|
|
111
|
-
```
|
|
112
|
-
|
|
120
|
+
```js
|
|
121
|
+
deleteItem("users", 1);
|
|
113
122
|
```
|
|
114
123
|
|
|
124
|
+
---
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
## ð Example Endpoints
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
| Action | Endpoint |
|
|
129
|
+
| ------ | ----------------- |
|
|
130
|
+
| Fetch | GET /users |
|
|
131
|
+
| Create | POST /users |
|
|
132
|
+
| Update | PUT /users/:id |
|
|
133
|
+
| Delete | DELETE /users/:id |
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## ð§Š Works With
|
|
138
|
+
|
|
139
|
+
* REST APIs
|
|
140
|
+
* Laravel / Express / Django
|
|
141
|
+
* Admin dashboards
|
|
142
|
+
* School / Business management systems
|
|
143
|
+
* Small to mid projects
|
|
125
144
|
|
|
126
145
|
---
|
|
127
146
|
|
|
128
|
-
##
|
|
147
|
+
## ð§Đ Philosophy
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
â ââ config.js # baseUrl & token config
|
|
134
|
-
â ââ http.js # fetch wrapper
|
|
135
|
-
â ââ store.js # central data store
|
|
136
|
-
â ââ crud.js # create / update / delete
|
|
137
|
-
â ââ index.js # exports
|
|
138
|
-
```
|
|
149
|
+
> Simple cache + smart subscribers
|
|
150
|
+
> No unnecessary abstraction
|
|
151
|
+
> Let React re-render naturally
|
|
139
152
|
|
|
140
153
|
---
|
|
141
154
|
|
|
142
|
-
##
|
|
155
|
+
## ð License
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
MIT ÂĐ Tarequl Islam
|
|
145
158
|
|
|
146
|
-
```js
|
|
147
|
-
export const config = {
|
|
148
|
-
baseUrl: "",
|
|
149
|
-
getToken: null,
|
|
150
|
-
notify: null // â
toast handler
|
|
151
|
-
}
|
|
152
159
|
|
|
153
|
-
export function setupCrud(options = {}) {
|
|
154
|
-
config.baseUrl = options.baseUrl || ""
|
|
155
|
-
config.getToken = options.getToken || null
|
|
156
|
-
config.notify = options.notify || null
|
|
157
|
-
}
|
|
158
160
|
|
|
159
|
-
```
|
|
160
161
|
|
|
161
|
-
### Why this design?
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
* `getToken` â optional, dynamic
|
|
165
|
-
* Works with:
|
|
163
|
+
## â
REAL-WORLD EXAMPLE (Vite + React)
|
|
166
164
|
|
|
167
|
-
|
|
168
|
-
* Cookie-based auth
|
|
169
|
-
* Public APIs
|
|
165
|
+
### ð `UserPage.jsx`
|
|
170
166
|
|
|
171
|
-
|
|
167
|
+
```jsx
|
|
168
|
+
import { useCrud, createItem, deleteItem } from "react-smart-crud";
|
|
172
169
|
|
|
173
|
-
|
|
170
|
+
export default function UserPage() {
|
|
171
|
+
const { data: users, loading, error } = useCrud("users");
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
export async function request(url, options = {}) {
|
|
179
|
-
const headers = {
|
|
180
|
-
"Content-Type": "application/json",
|
|
181
|
-
...(options.headers || {}),
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// ð token optional
|
|
185
|
-
if (config.getToken) {
|
|
186
|
-
const token = config.getToken();
|
|
187
|
-
if (token) {
|
|
188
|
-
headers.Authorization = `Bearer ${token}`;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
173
|
+
if (loading) return <p>Loading...</p>;
|
|
174
|
+
if (error) return <p>Something went wrong</p>;
|
|
191
175
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// ðĒ body safe parse
|
|
198
|
-
const data = await res.json().catch(() => ({}));
|
|
199
|
-
|
|
200
|
-
// ðī IMPORTANT FIX
|
|
201
|
-
if (!res.ok) {
|
|
202
|
-
throw {
|
|
203
|
-
status: res.status,
|
|
204
|
-
message: data.message || "Something went wrong",
|
|
205
|
-
data,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
176
|
+
return (
|
|
177
|
+
<div style={{ padding: 20 }}>
|
|
178
|
+
<h2>Users</h2>
|
|
208
179
|
|
|
209
|
-
|
|
180
|
+
<button
|
|
181
|
+
onClick={() =>
|
|
182
|
+
createItem("users", {
|
|
183
|
+
name: "New User",
|
|
184
|
+
email: "test@mail.com",
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
â Add User
|
|
189
|
+
</button>
|
|
190
|
+
|
|
191
|
+
<ul>
|
|
192
|
+
{users.map((u) => (
|
|
193
|
+
<li key={u.id}>
|
|
194
|
+
{u.name}
|
|
195
|
+
<button onClick={() => deleteItem("users", u.id)}>
|
|
196
|
+
â
|
|
197
|
+
</button>
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
210
203
|
}
|
|
204
|
+
````
|
|
211
205
|
|
|
212
|
-
|
|
206
|
+
|
|
207
|
+
---
|
|
213
208
|
|
|
214
|
-
|
|
209
|
+
## ðĨ Optimistic UI â Full Explanation (ADD THIS)
|
|
215
210
|
|
|
216
|
-
|
|
217
|
-
â UI controls error display
|
|
218
|
-
â No generic error forcing
|
|
211
|
+
### ðŊ Why Optimistic UI?
|
|
219
212
|
|
|
220
|
-
|
|
213
|
+
Optimistic UI means:
|
|
221
214
|
|
|
222
|
-
|
|
215
|
+
> **Server response āĶāĶļāĶūāͰ āĶāĶā§āĶ UI update āĶđāĶŽā§**
|
|
216
|
+
> Error āĶđāĶēā§ auto rollback āĶđāĶŽā§
|
|
223
217
|
|
|
224
|
-
|
|
225
|
-
* Shared across all components
|
|
226
|
-
* Subscribers auto re-render
|
|
218
|
+
react-smart-crud āĶ āĶāĶāĶū **fully optional**āĨĪ
|
|
227
219
|
|
|
228
|
-
|
|
220
|
+
---
|
|
229
221
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
```
|
|
234
233
|
|
|
235
234
|
---
|
|
236
235
|
|
|
237
|
-
##
|
|
236
|
+
## ðĒ CREATE with Optimistic UI
|
|
238
237
|
|
|
239
|
-
###
|
|
238
|
+
### Example
|
|
240
239
|
|
|
241
240
|
```js
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
258
|
```
|
|
259
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
|
+
|
|
260
269
|
---
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## ð UPDATE with Optimistic UI (Advanced)
|
|
261
273
|
|
|
262
|
-
###
|
|
274
|
+
### Example
|
|
263
275
|
|
|
264
276
|
```js
|
|
265
277
|
updateItem(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
285
305
|
```
|
|
286
306
|
|
|
307
|
+
â You control exactly how UI changes
|
|
308
|
+
â Useful for forms, partial updates, toggle switches
|
|
309
|
+
|
|
287
310
|
---
|
|
288
311
|
|
|
289
|
-
|
|
312
|
+
## â DELETE with Manual Error Handling
|
|
290
313
|
|
|
291
314
|
```js
|
|
292
|
-
deleteItem("users", id, {
|
|
315
|
+
deleteItem("users", user.id, {
|
|
316
|
+
onSuccess: () => toast.success("Deleted"),
|
|
293
317
|
onError: (err) => toast.error(err.message),
|
|
294
318
|
});
|
|
295
319
|
```
|
|
296
320
|
|
|
297
321
|
---
|
|
298
322
|
|
|
299
|
-
##
|
|
323
|
+
## ð Toast / Notification Integration
|
|
300
324
|
|
|
301
|
-
|
|
302
|
-
2. API request is sent
|
|
303
|
-
3. Server success â finalize data
|
|
304
|
-
4. Server error â rollback
|
|
305
|
-
5. Subscribers re-render automatically
|
|
325
|
+
You can use:
|
|
306
326
|
|
|
307
|
-
|
|
308
|
-
No flicker
|
|
309
|
-
No confusion
|
|
327
|
+
* react-hot-toast
|
|
310
328
|
|
|
311
329
|
---
|
|
312
330
|
|
|
313
|
-
##
|
|
331
|
+
## ð§ One-time Setup for Toast
|
|
314
332
|
|
|
315
|
-
###
|
|
333
|
+
### `src/smartCrudConfig.js`
|
|
316
334
|
|
|
317
335
|
```js
|
|
318
|
-
|
|
336
|
+
import { setupCrud } from "react-smart-crud";
|
|
337
|
+
import toast from "react-hot-toast";
|
|
338
|
+
|
|
339
|
+
setupCrud({
|
|
340
|
+
baseUrl: "https://your-api.com",
|
|
341
|
+
|
|
342
|
+
notify: (type, message) => {
|
|
343
|
+
if (type === "success") toast.success(message);
|
|
344
|
+
if (type === "error") toast.error(message);
|
|
345
|
+
},
|
|
346
|
+
});
|
|
319
347
|
```
|
|
320
348
|
|
|
321
|
-
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## ð§ Manual vs Automatic Notifications
|
|
352
|
+
|
|
353
|
+
### Automatic (inside library)
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
notify("success", "Deleted");
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Manual (recommended)
|
|
322
360
|
|
|
323
361
|
```js
|
|
324
|
-
|
|
362
|
+
createItem("users", data, {
|
|
363
|
+
onSuccess: () => toast.success("Created"),
|
|
364
|
+
onError: (err) => toast.error(err.message),
|
|
365
|
+
});
|
|
325
366
|
```
|
|
326
367
|
|
|
327
|
-
â
|
|
328
|
-
â
|
|
368
|
+
â Full control
|
|
369
|
+
â Better UX
|
|
370
|
+
â No magic
|
|
329
371
|
|
|
330
372
|
---
|
|
331
373
|
|
|
332
|
-
|
|
374
|
+
# ð§Đ Summary Table (ADD THIS)
|
|
333
375
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
376
|
+
| Action | Optimistic | Rollback | Manual Toast |
|
|
377
|
+
| ---------- | ------------------ | -------- | ------------ |
|
|
378
|
+
| createItem | â
| â
| â
|
|
|
379
|
+
| updateItem | â
| â
| â
|
|
|
380
|
+
| deleteItem | â (instant remove) | â
| â
|
|
|
338
381
|
|
|
339
382
|
---
|
|
340
383
|
|
|
341
|
-
|
|
384
|
+
# ðĄ Best Practices (Pro Tips)
|
|
342
385
|
|
|
343
|
-
|
|
344
|
-
* Multiple components use same data
|
|
345
|
-
* Manual syncing is fragile
|
|
386
|
+
â Always return **full object** from optimistic update
|
|
346
387
|
|
|
347
|
-
|
|
388
|
+
â Keep optimistic logic **UI-only**
|
|
348
389
|
|
|
349
|
-
|
|
390
|
+
â Never trust optimistic data as server truth
|
|
350
391
|
|
|
351
|
-
|
|
352
|
-
* Components subscribe directly
|
|
353
|
-
* Clean and scalable
|
|
392
|
+
â Handle toast in component, not inside library
|
|
354
393
|
|
|
355
394
|
---
|
|
356
395
|
|
|
357
|
-
## ð Best Use Cases
|
|
358
396
|
|
|
359
|
-
* Admin dashboards
|
|
360
|
-
* Management systems
|
|
361
|
-
* Internal tools
|
|
362
|
-
* CRUD-heavy applications
|
|
363
|
-
* Rapid MVPs
|
|
364
397
|
|
|
365
|
-
---
|
|
366
398
|
|
|
367
|
-
## âĪïļ Philosophy
|
|
368
399
|
|
|
369
|
-
> Simple tools scale better than complex abstractions.
|
|
370
400
|
|
|
371
|
-
No magic
|
|
372
|
-
No hidden behavior
|
|
373
|
-
Just predictable CRUD
|
|
374
401
|
|
|
375
|
-
---
|
|
376
402
|
|
|
377
|
-
## ð License
|
|
378
403
|
|
|
379
|
-
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
380
411
|
|
|
381
412
|
---
|
|
382
413
|
|
|
383
|
-
##
|
|
414
|
+
## â
How it works (Mental Model)
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
Component
|
|
418
|
+
â
|
|
419
|
+
useCrud("users")
|
|
420
|
+
â
|
|
421
|
+
Global store cache
|
|
422
|
+
â
|
|
423
|
+
API request (once)
|
|
424
|
+
â
|
|
425
|
+
All subscribers auto update
|
|
426
|
+
```
|
|
384
427
|
|
|
385
|
-
|
|
386
|
-
you already understand **react-smart-crud**.
|
|
428
|
+
ð Multiple components â **same data, no duplicate fetch**
|
|
387
429
|
|
|
388
|
-
|
|
430
|
+
---
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-smart-crud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Minimal optimistic CRUD helper for React without useState, useEffect, or prop drilling",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
],
|
|
5
|
+
"type": "./src/index.d.ts",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
11
8
|
"keywords": [
|
|
12
9
|
"react",
|
|
13
10
|
"crud",
|
|
@@ -17,15 +14,21 @@
|
|
|
17
14
|
"admin-dashboard"
|
|
18
15
|
],
|
|
19
16
|
"author": "Tarequl Islam",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "https://github.com/tareq-dev/react-smart-crud"
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./src/index.js"
|
|
24
19
|
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
25
23
|
"peerDependencies": {
|
|
26
24
|
"react": ">=16.8"
|
|
27
25
|
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/tareq-dev/react-smart-crud.git"
|
|
29
|
+
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"vite": "^7.3.0"
|
|
30
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
31
34
|
}
|
package/src/actions.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getEntry } from "./store";
|
|
2
|
+
import { request } from "./http";
|
|
3
|
+
import { notify } from "./notify";
|
|
4
|
+
|
|
5
|
+
/* ================= CREATE ================= */
|
|
6
|
+
|
|
7
|
+
export function createItem(url, data, options = {}) {
|
|
8
|
+
const entry = getEntry(url);
|
|
9
|
+
|
|
10
|
+
/* ========== OPTIMISTIC ========== */
|
|
11
|
+
const optimisticData = options.optimistic ? options.optimistic(data) : data;
|
|
12
|
+
|
|
13
|
+
const tempItem = {
|
|
14
|
+
id: Date.now(),
|
|
15
|
+
...optimisticData,
|
|
16
|
+
_temp: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
entry.data = [tempItem, ...entry.data];
|
|
20
|
+
entry.subscribers.forEach((fn) => fn());
|
|
21
|
+
|
|
22
|
+
/* ========== REQUEST ========== */
|
|
23
|
+
request(url, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify(data),
|
|
27
|
+
})
|
|
28
|
+
.then((serverData) => {
|
|
29
|
+
// â
MERGE â replace āĶĻāĶū
|
|
30
|
+
entry.data = entry.data.map((item) =>
|
|
31
|
+
item._temp ? { ...item, ...serverData, _temp: false } : item
|
|
32
|
+
);
|
|
33
|
+
notify("success", "Created", { url, data: serverData });
|
|
34
|
+
options.onSuccess?.(serverData);
|
|
35
|
+
})
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
// ðī rollback
|
|
38
|
+
entry.data = entry.data.filter((i) => !i._temp);
|
|
39
|
+
notify("error", error.message || "Create failed", {
|
|
40
|
+
url,
|
|
41
|
+
error,
|
|
42
|
+
});
|
|
43
|
+
options.onError?.(error);
|
|
44
|
+
})
|
|
45
|
+
.finally(() => {
|
|
46
|
+
entry.subscribers.forEach((fn) => fn());
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ================= UPDATE ================= */
|
|
51
|
+
export function updateItem(url, id, data, options = {}) {
|
|
52
|
+
const entry = getEntry(url);
|
|
53
|
+
const backup = [...entry.data];
|
|
54
|
+
|
|
55
|
+
// ðĒ OPTIMISTIC UPDATE
|
|
56
|
+
entry.data = entry.data.map((item) =>
|
|
57
|
+
item.id === id
|
|
58
|
+
? {
|
|
59
|
+
...(options.optimistic
|
|
60
|
+
? options.optimistic(item, data)
|
|
61
|
+
: { ...item, ...data }),
|
|
62
|
+
_updating: true,
|
|
63
|
+
}
|
|
64
|
+
: item
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
entry.subscribers.forEach((fn) => fn());
|
|
68
|
+
|
|
69
|
+
request(`${url}/${id}`, {
|
|
70
|
+
method: "PUT",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify(data),
|
|
73
|
+
})
|
|
74
|
+
.then((serverData) => {
|
|
75
|
+
// ðĩ Merge server data, overwrite āĶĻāĶū
|
|
76
|
+
entry.data = entry.data.map((item) =>
|
|
77
|
+
item.id === id ? { ...item, ...serverData, _updating: false } : item
|
|
78
|
+
);
|
|
79
|
+
notify("success", "Updated", { url, id, data: serverData });
|
|
80
|
+
options.onSuccess?.(serverData);
|
|
81
|
+
})
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
entry.data = backup;
|
|
84
|
+
|
|
85
|
+
notify("error", error.message || "Update failed", {
|
|
86
|
+
url,
|
|
87
|
+
id,
|
|
88
|
+
error,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
options.onError?.(error);
|
|
92
|
+
})
|
|
93
|
+
.finally(() => entry.subscribers.forEach((fn) => fn()));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ================= DELETE ================= */
|
|
97
|
+
export function deleteItem(url, id, options = {}) {
|
|
98
|
+
const entry = getEntry(url);
|
|
99
|
+
const backup = [...entry.data];
|
|
100
|
+
|
|
101
|
+
entry.data = entry.data.filter((i) => i.id !== id);
|
|
102
|
+
entry.subscribers.forEach((fn) => fn());
|
|
103
|
+
|
|
104
|
+
request(`${url}/${id}`, { method: "DELETE" })
|
|
105
|
+
.then(() => {
|
|
106
|
+
options.onSuccess?.();
|
|
107
|
+
notify("success", "Deleted");
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
entry.data = backup;
|
|
111
|
+
entry.subscribers.forEach((fn) => fn());
|
|
112
|
+
options.onError?.(error);
|
|
113
|
+
});
|
|
114
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
baseUrl: "",
|
|
3
|
+
getToken: null,
|
|
4
|
+
notify: null // â
toast handler
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setupCrud(options = {}) {
|
|
8
|
+
config.baseUrl = options.baseUrl || ""
|
|
9
|
+
config.getToken = options.getToken || null
|
|
10
|
+
config.notify = options.notify || null
|
|
11
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { config } from "./config";
|
|
2
|
+
|
|
3
|
+
export async function request(url, options = {}) {
|
|
4
|
+
const headers = {
|
|
5
|
+
"Content-Type": "application/json",
|
|
6
|
+
...(options.headers || {}),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// ð token optional
|
|
10
|
+
if (config.getToken) {
|
|
11
|
+
const token = config.getToken();
|
|
12
|
+
if (token) {
|
|
13
|
+
headers.Authorization = `Bearer ${token}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const res = await fetch(config.baseUrl + url, {
|
|
18
|
+
...options,
|
|
19
|
+
headers,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ðĒ body safe parse
|
|
23
|
+
const data = await res.json().catch(() => ({}));
|
|
24
|
+
|
|
25
|
+
// ðī IMPORTANT FIX
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw {
|
|
28
|
+
status: res.status,
|
|
29
|
+
message: data.message || "Something went wrong",
|
|
30
|
+
data,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return data;
|
|
35
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare module "react-smart-crud" {
|
|
2
|
+
export function setupCrud(options: {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
getToken?: () => string | null;
|
|
5
|
+
notify?: (type: string, message: string, meta?: any) => void;
|
|
6
|
+
}): void;
|
|
7
|
+
|
|
8
|
+
export function useCrud<T = any>(url: string): {
|
|
9
|
+
data: T[];
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: any;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createItem(
|
|
15
|
+
url: string,
|
|
16
|
+
data: any,
|
|
17
|
+
options?: any
|
|
18
|
+
): void;
|
|
19
|
+
|
|
20
|
+
export function updateItem(
|
|
21
|
+
url: string,
|
|
22
|
+
id: number | string,
|
|
23
|
+
data: any,
|
|
24
|
+
options?: any
|
|
25
|
+
): void;
|
|
26
|
+
|
|
27
|
+
export function deleteItem(
|
|
28
|
+
url: string,
|
|
29
|
+
id: number | string,
|
|
30
|
+
options?: any
|
|
31
|
+
): void;
|
|
32
|
+
}
|
package/src/index.js
ADDED
package/src/notify.js
ADDED
package/src/store.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const store = new Map();
|
|
2
|
+
|
|
3
|
+
export function getEntry(key) {
|
|
4
|
+
// console.log("ENTRY KEY:", key);
|
|
5
|
+
if (!store.has(key)) {
|
|
6
|
+
store.set(key, {
|
|
7
|
+
data: [],
|
|
8
|
+
loading: false,
|
|
9
|
+
error: null,
|
|
10
|
+
subscribers: new Set(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return store.get(key);
|
|
14
|
+
}
|
package/src/useCrud.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getEntry } from "./store";
|
|
3
|
+
import { request } from "./http";
|
|
4
|
+
|
|
5
|
+
export function useCrud(url) {
|
|
6
|
+
const entry = getEntry(url);
|
|
7
|
+
const [, force] = useState(0);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const rerender = () => force((x) => x + 1);
|
|
11
|
+
entry.subscribers.add(rerender);
|
|
12
|
+
|
|
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
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => entry.subscribers.delete(rerender);
|
|
31
|
+
}, [url]);
|
|
32
|
+
|
|
33
|
+
// ð IMPORTANT: return new reference
|
|
34
|
+
return {
|
|
35
|
+
data: entry.data,
|
|
36
|
+
loading: entry.loading,
|
|
37
|
+
error: entry.error,
|
|
38
|
+
};
|
|
39
|
+
}
|