use-abcd 1.4.0 → 1.4.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![Build Status](https://github.com/smtrd3/common-state/workflows/CI/badge.svg)](https://github.com/smtrd3/common-state/actions)
4
4
 
5
+ Most apps on the internet are some form of CRUD - whether it's a todo list, a dashboard, or a social media feed. Yet we often find ourselves fighting with complex state management frameworks, reinventing patterns for each project. What if we inverted the problem? Instead of building custom state management, recognize that your app state is likely CRUD at its core. This library provides highly optimized, systematic CRUD state management that makes your code easier to reason about and your life as a developer much simpler. You don't need to invent state management for each app - chances are, it's just CRUD.
6
+
5
7
  A powerful React hook for managing ABCD (or CRUD) operations with optimistic updates, caching, and automatic state management.
6
8
 
7
9
  > **Note on Package Name**: The package is published as `use-abcd` on npm due to naming availability, where ABCD stands for Add, Browse, Change, and Delete - which maps directly to the traditional CRUD (Create, Read, Update, Delete) operations. While the package name uses ABCD, all internal APIs and documentation use CRUD terminology for familiarity and consistency with common programming patterns.
@@ -15,6 +17,7 @@ A powerful React hook for managing ABCD (or CRUD) operations with optimistic upd
15
17
  - ⏳ Debounced sync with configurable delays
16
18
  - 🔄 Sync queue management with pause/resume/retry
17
19
  - 🎨 Context-based filtering and pagination
20
+ - 🔌 End-to-end type-safe client-server sync utilities
18
21
 
19
22
  ## Installation
20
23
 
@@ -88,10 +91,11 @@ function TodoList() {
88
91
  The `Config` object defines how your data is fetched, synced, and managed:
89
92
 
90
93
  ```typescript
91
- type Config<T, C> = {
94
+ type Config<T extends object, C> = {
92
95
  id: string; // Unique identifier for this collection
93
96
  initialContext: C; // Initial context (filters, pagination, etc.)
94
97
  getId: (item: T) => string; // Extract ID from item
98
+ setId?: (item: T, newId: string) => T; // Optional: for ID remapping on create
95
99
 
96
100
  // Optional sync configuration
97
101
  syncDebounce?: number; // Debounce delay for sync (default: 300ms)
@@ -103,7 +107,7 @@ type Config<T, C> = {
103
107
 
104
108
  // Required handlers
105
109
  onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
106
- onSync: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
110
+ onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
107
111
  };
108
112
  ```
109
113
 
@@ -119,7 +123,6 @@ The `useCrud` hook returns:
119
123
  loading: boolean; // Fetch loading state
120
124
  syncing: boolean; // Sync in progress
121
125
  syncQueue: SyncQueueState<T>; // Sync queue state
122
- syncState: SyncState; // Overall sync state
123
126
 
124
127
  // Item operations (optimistic)
125
128
  create: (item: T) => void;
@@ -141,126 +144,63 @@ The `useCrud` hook returns:
141
144
 
142
145
  ## Examples
143
146
 
144
- The repository includes comprehensive examples demonstrating various use cases:
145
-
146
- ### 1. Full CRUD Operations (Products Example)
147
-
148
- Demonstrates complete CRUD functionality with:
149
- - Create, read, update, delete operations
150
- - Category filtering and search
151
- - Sync queue management
152
- - Error handling with retries
153
- - Per-item status indicators
154
-
155
- ```typescript
156
- const ProductsConfig: Config<Product, ProductContext> = {
157
- id: "products",
158
- initialContext: { page: 1, limit: 10 },
159
- getId: (item) => item.id,
160
-
161
- onFetch: async (context, signal) => {
162
- const params = new URLSearchParams({
163
- page: String(context.page),
164
- limit: String(context.limit),
165
- });
166
- if (context.category) params.append("category", context.category);
167
- if (context.search) params.append("search", context.search);
147
+ The repository includes examples demonstrating various use cases:
168
148
 
169
- const response = await fetch(`/api/products?${params}`, { signal });
170
- return (await response.json()).items;
171
- },
149
+ - **Products** - Full CRUD with filtering, search, and error handling
150
+ - **Pagination** - Context-based pagination with dynamic page size
151
+ - **Optimistic Updates** - Comments with instant UI feedback and sync queue visualization
152
+ - **Blog Post** - Simple single-item editing
153
+ - **Todo List** - Basic list operations
172
154
 
173
- onSync: async (changes, signal) => {
174
- // Handle batch sync operations
175
- },
176
- };
177
- ```
155
+ Run locally: `bun run dev` or `npm run dev` and visit `http://localhost:5173`
178
156
 
179
- **Key features:**
180
- - Filtering by category
181
- - Text search
182
- - Pause/resume sync
183
- - Retry failed operations
184
- - Visual status indicators
157
+ ## Advanced Usage
185
158
 
186
- ### 2. Pagination (Users Example)
159
+ ### Individual Item Management
187
160
 
188
- Shows context-based pagination with:
189
- - Dynamic page size selection
190
- - Next/previous navigation
191
- - Context updates trigger re-fetch
161
+ Use `getItem()` with `useItem()` for managing individual items:
192
162
 
193
163
  ```typescript
194
- interface UserContext {
195
- page: number;
196
- limit: number;
164
+ import { useCrud, useItem } from "use-abcd";
165
+
166
+ function ProductList() {
167
+ const { items, getItem } = useCrud(ProductsConfig);
168
+
169
+ return (
170
+ <div>
171
+ {Array.from(items.keys()).map((id) => (
172
+ <ProductItem key={id} item={getItem(id)} />
173
+ ))}
174
+ </div>
175
+ );
197
176
  }
198
177
 
199
- const { items, context, setContext } = useCrud<User, UserContext>(UsersConfig);
200
-
201
- // Change page
202
- setContext((draft) => {
203
- draft.page += 1;
204
- });
205
-
206
- // Change items per page
207
- setContext((draft) => {
208
- draft.limit = 20;
209
- draft.page = 1; // Reset to first page
210
- });
211
- ```
212
-
213
- **Key features:**
214
- - Configurable page size
215
- - Context-based pagination
216
- - Automatic re-fetch on context change
217
-
218
- ### 3. Optimistic Updates (Comments Example)
219
-
220
- Demonstrates the power of optimistic updates:
221
- - Instant UI feedback
222
- - Background synchronization
223
- - Sync queue visualization
224
- - Manual retry controls
225
- - Error state handling
226
-
227
- ```typescript
228
- const CommentsConfig: Config<Comment, CommentContext> = {
229
- id: "comments-optimistic",
230
- initialContext: { postId: "1" },
231
- getId: (item) => item.id,
232
- syncDebounce: 100, // Very short debounce for demo
233
-
234
- // ... handlers
235
- };
236
-
237
- // Create appears instantly in UI
238
- create({
239
- id: `temp-${Date.now()}`,
240
- text: "New comment",
241
- author: "You",
242
- createdAt: new Date().toISOString(),
243
- });
178
+ function ProductItem({ item }: { item: Item<Product, ProductContext> }) {
179
+ const { data, status, update, remove, exists } = useItem(item);
180
+
181
+ if (!exists) return null;
182
+
183
+ return (
184
+ <div>
185
+ <h3>{data?.name}</h3>
186
+ <p>Status: {status?.status || "synced"}</p>
187
+ <button onClick={() => update((draft) => { draft.stock += 1; })}>
188
+ Add Stock
189
+ </button>
190
+ <button onClick={() => remove()}>Delete</button>
191
+ </div>
192
+ );
193
+ }
244
194
  ```
245
195
 
246
- **Key features:**
247
- - Immediate UI updates
248
- - Sync queue status display
249
- - Pause/resume synchronization
250
- - Per-item sync status
251
- - Manual retry for errors
252
-
253
- ### 4. Original Examples
254
-
255
- The repository also includes the original simpler examples:
256
- - **Blog Post**: Single item editing with optimistic updates
257
- - **Todo List**: Simple list with toggle completion
196
+ **Benefits:**
197
+ - `useItem()` subscribes only to that specific item's changes
198
+ - React re-renders only when that item's data changes (automatic optimization via WeakMap cache)
199
+ - Clean separation of list and item concerns
258
200
 
259
- ## Advanced Usage
260
-
261
- ### Custom Context for Filtering
201
+ ### Context-Based Filtering & Pagination
262
202
 
263
- Use context to manage filters, pagination, sorting, etc.:
203
+ Use context to manage filters, pagination, sorting:
264
204
 
265
205
  ```typescript
266
206
  interface ProductContext {
@@ -268,228 +208,271 @@ interface ProductContext {
268
208
  limit: number;
269
209
  category?: string;
270
210
  search?: string;
271
- sortBy?: "name" | "price";
272
211
  }
273
212
 
274
- const { context, setContext } = useCrud<Product, ProductContext>(config);
213
+ const { items, context, setContext } = useCrud<Product, ProductContext>(config);
275
214
 
276
- // Update multiple context fields
215
+ // Update context to refetch
277
216
  setContext((draft) => {
278
217
  draft.category = "electronics";
279
218
  draft.page = 1;
280
219
  });
281
220
  ```
282
221
 
283
- ### Monitoring Sync Queue
222
+ ### Sync Queue Monitoring
284
223
 
285
224
  Track pending changes and errors:
286
225
 
287
226
  ```typescript
288
227
  const { syncQueue, pauseSync, resumeSync, retrySync } = useCrud(config);
289
228
 
229
+ // Check queue state
290
230
  console.log({
291
231
  pending: syncQueue.queue.size,
292
232
  inFlight: syncQueue.inFlight.size,
293
233
  errors: syncQueue.errors.size,
294
- isPaused: syncQueue.isPaused,
295
- isSyncing: syncQueue.isSyncing,
296
234
  });
297
235
 
298
- // Pause sync temporarily
236
+ // Control sync
299
237
  pauseSync();
300
-
301
- // Resume sync
302
238
  resumeSync();
303
-
304
- // Retry specific item
305
- retrySync(itemId);
306
-
307
- // Retry all failed items
308
- retrySync();
309
- ```
310
-
311
- ### Per-Item Status
312
-
313
- Track the sync status of individual items:
314
-
315
- ```typescript
316
- const { getItemStatus } = useCrud(config);
317
-
318
- const status = getItemStatus(itemId);
319
- if (status) {
320
- console.log({
321
- type: status.type, // "create" | "update" | "delete"
322
- status: status.status, // "pending" | "syncing" | "success" | "error"
323
- retries: status.retries, // Number of retry attempts
324
- error: status.error, // Error message if failed
325
- });
326
- }
239
+ retrySync(); // Retry all failed items
240
+ retrySync(itemId); // Retry specific item
327
241
  ```
328
242
 
329
243
  ### ID Remapping for Optimistic Creates
330
244
 
331
- When creating items optimistically, you typically use a temporary ID (e.g., `temp-${Date.now()}`). After the server confirms the creation, it may assign a different permanent ID. The library automatically handles this ID remapping.
332
-
333
- **In your `onSync` handler**, return the server-assigned `newId` for create operations:
245
+ Handle temporary IDs that get replaced by server-assigned IDs:
334
246
 
335
247
  ```typescript
336
248
  onSync: async (changes, signal) => {
337
- const results: SyncResult[] = [];
338
-
339
249
  for (const change of changes) {
340
250
  if (change.type === "create") {
341
251
  const response = await fetch("/api/items", {
342
252
  method: "POST",
343
- headers: { "Content-Type": "application/json" },
344
253
  body: JSON.stringify(change.data),
345
254
  signal,
346
255
  });
347
-
348
- if (!response.ok) throw new Error("Failed to create");
349
-
350
256
  const data = await response.json();
351
- // Return newId to remap the temporary ID to the server-assigned ID
352
- results.push({
353
- id: change.id, // The temporary ID
257
+
258
+ // Return newId to remap temp ID to server ID
259
+ return {
260
+ id: change.id, // Temporary ID (e.g., "temp-123")
354
261
  status: "success",
355
- newId: data.id, // The server-assigned permanent ID
356
- });
262
+ newId: data.id, // Server-assigned ID (e.g., "456")
263
+ };
357
264
  }
358
265
  // ... handle update and delete
359
266
  }
360
-
361
- return results;
362
267
  };
363
268
  ```
364
269
 
365
- **What happens automatically:**
366
- 1. The item's key in the `items` Map is updated from `temp-123` to `server-456`
367
- 2. The item's `id` property is updated (assumes item has an `id` field)
368
- 3. Any `Item` references are updated to use the new ID
369
- 4. The UI re-renders with the correct permanent ID
270
+ The library automatically:
271
+ 1. Updates the item's key in the `items` Map
272
+ 2. Updates the item's `id` property
273
+ 3. Updates any `Item` references
274
+ 4. Triggers UI re-render
370
275
 
371
- **Custom ID field**: If your item uses a different property for the ID (not `id`), provide a `setId` function in your config:
276
+ ## End-to-End Type-Safe CRUD with createSyncClient & createSyncServer
277
+
278
+ Build a complete type-safe CRUD solution with minimal boilerplate:
279
+
280
+ ### Client Setup
372
281
 
373
282
  ```typescript
374
- const config: Config<MyItem, Context> = {
375
- getId: (item) => item.itemId,
376
- setId: (item, newId) => ({ ...item, itemId: newId }),
377
- // ...
283
+ import { useCrud, createSyncClientFromEndpoint } from "use-abcd";
284
+
285
+ interface User {
286
+ id: string;
287
+ name: string;
288
+ email: string;
289
+ }
290
+
291
+ interface UserQuery {
292
+ page: number;
293
+ limit: number;
294
+ search?: string;
295
+ }
296
+
297
+ const UserConfig: Config<User, UserQuery> = {
298
+ id: "users",
299
+ initialContext: { page: 1, limit: 10 },
300
+ getId: (user) => user.id,
301
+
302
+ // Use createSyncClientFromEndpoint for unified fetch + sync
303
+ ...createSyncClientFromEndpoint<User, UserQuery>("/api/users"),
378
304
  };
379
- ```
380
305
 
381
- ### Cache Control
306
+ function UserList() {
307
+ const { items, loading, create, update, remove, setContext } = useCrud(UserConfig);
308
+
309
+ return (
310
+ <div>
311
+ {loading ? <p>Loading...</p> : (
312
+ <>
313
+ {Array.from(items.values()).map((user) => (
314
+ <div key={user.id}>
315
+ <span>{user.name} - {user.email}</span>
316
+ <button onClick={() => update(user.id, (draft) => {
317
+ draft.name = "Updated Name";
318
+ })}>
319
+ Update
320
+ </button>
321
+ <button onClick={() => remove(user.id)}>Delete</button>
322
+ </div>
323
+ ))}
324
+ <button onClick={() => create({
325
+ id: `temp-${Date.now()}`,
326
+ name: "New User",
327
+ email: "new@example.com",
328
+ })}>
329
+ Add User
330
+ </button>
331
+ </>
332
+ )}
333
+ </div>
334
+ );
335
+ }
336
+ ```
382
337
 
383
- Control caching behavior:
338
+ ### Server Setup
384
339
 
385
340
  ```typescript
386
- const config: Config<T, C> = {
387
- // ...
388
- cacheCapacity: 20, // Store up to 20 cache entries
389
- cacheTtl: 30000, // Cache expires after 30 seconds
390
- };
341
+ import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
391
342
 
392
- const { refresh } = useCrud(config);
343
+ // Define your handlers
344
+ const usersHandler = createSyncServer<User, UserQuery>({
345
+ fetch: async (query) => {
346
+ // Handle pagination and search
347
+ return db.users.findMany({
348
+ skip: (query.page - 1) * query.limit,
349
+ take: query.limit,
350
+ where: query.search ? { name: { contains: query.search } } : undefined,
351
+ });
352
+ },
393
353
 
394
- // Force refresh (bypass cache)
395
- await refresh();
396
- ```
354
+ create: async (data) => {
355
+ const user = await db.users.create({ data });
356
+ return serverSyncSuccess({ newId: user.id });
357
+ },
397
358
 
398
- ## Running Examples Locally
359
+ update: async (id, data) => {
360
+ await db.users.update({ where: { id }, data });
361
+ return serverSyncSuccess();
362
+ },
399
363
 
400
- The repository includes a development environment with MSW (Mock Service Worker) for testing:
364
+ delete: async (id) => {
365
+ await db.users.delete({ where: { id } });
366
+ return serverSyncSuccess();
367
+ },
368
+ });
401
369
 
402
- ```bash
403
- # Clone the repository
404
- git clone https://github.com/smtrd3/use-abcd
405
- cd use-abcd
370
+ // Use with your framework
371
+ // Next.js App Router
372
+ export const POST = usersHandler.handler;
406
373
 
407
- # Install dependencies
408
- bun install # or npm install
374
+ // Hono
375
+ app.post("/api/users", (c) => usersHandler.handler(c.req.raw));
409
376
 
410
- # Start development server
411
- bun run dev # or npm run dev
377
+ // Bun.serve
378
+ Bun.serve({
379
+ fetch(req) {
380
+ if (new URL(req.url).pathname === "/api/users") {
381
+ return usersHandler.handler(req);
382
+ }
383
+ }
384
+ });
412
385
  ```
413
386
 
414
- Visit `http://localhost:5173` to see the examples in action.
387
+ ### What You Get
388
+
389
+ - **Type safety**: Full TypeScript inference from data types to API calls
390
+ - **Automatic ID remapping**: Temporary IDs are replaced with server-assigned IDs
391
+ - **Batch operations**: Multiple changes are sent in a single request
392
+ - **Optimistic updates**: UI updates instantly, syncs in background
393
+ - **Error handling**: Failed operations are tracked and can be retried
394
+ - **Unified endpoint**: Single POST endpoint handles fetch + create/update/delete
415
395
 
416
- ### Available Examples:
396
+ ### Request/Response Format
417
397
 
418
- 1. **Products (Full CRUD)** - Complete CRUD operations with filtering
419
- 2. **Pagination** - Context-based pagination with users
420
- 3. **Optimistic Updates** - Comments with sync queue visualization
421
- 4. **Blog Post (Original)** - Simple single-item editing
422
- 5. **Todo (Original)** - Basic list operations
398
+ ```typescript
399
+ // Fetch + Sync in one request
400
+ POST /api/users
401
+ Body: {
402
+ query: { page: 1, limit: 10, search: "john" },
403
+ changes: [
404
+ { id: "temp-123", type: "create", data: { ... } },
405
+ { id: "456", type: "update", data: { ... } },
406
+ { id: "789", type: "delete", data: { ... } }
407
+ ]
408
+ }
409
+
410
+ Response: {
411
+ results: [...users], // Fetched items
412
+ syncResults: [ // Sync results
413
+ { id: "temp-123", status: "success", newId: "999" },
414
+ { id: "456", status: "success" },
415
+ { id: "789", status: "success" }
416
+ ]
417
+ }
418
+ ```
423
419
 
424
420
  ## API Reference
425
421
 
426
422
  ### Types
427
423
 
428
424
  ```typescript
429
- // Main configuration
430
- type Config<T, C> = {
425
+ type Config<T extends object, C> = {
431
426
  id: string;
432
427
  initialContext: C;
433
428
  getId: (item: T) => string;
434
- setId?: (item: T, newId: string) => T; // Optional: for ID remapping on create
429
+ setId?: (item: T, newId: string) => T;
435
430
  syncDebounce?: number;
436
431
  syncRetries?: number;
437
432
  cacheCapacity?: number;
438
433
  cacheTtl?: number;
439
434
  onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
440
- onSync: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
435
+ onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
441
436
  };
442
437
 
443
- // Change type for sync operations
444
438
  type Change<T> = {
445
439
  id: string;
446
440
  type: "create" | "update" | "delete";
447
441
  data: T;
448
442
  };
449
443
 
450
- // Sync result
451
444
  type SyncResult = {
452
445
  id: string;
453
446
  status: "success" | "error";
454
447
  error?: string;
455
- newId?: string; // For create operations: server-assigned ID to replace temp ID
448
+ newId?: string; // For creates: server-assigned ID
456
449
  };
457
450
 
458
- // Item status
459
451
  type ItemStatus = {
460
452
  type: "create" | "update" | "delete";
461
453
  status: "pending" | "syncing" | "success" | "error";
462
454
  retries: number;
463
455
  error?: string;
464
456
  } | null;
465
-
466
- // Sync queue state
467
- type SyncQueueState<T> = {
468
- queue: Map<string, Change<T>>; // Pending changes
469
- inFlight: Map<string, Change<T>>; // Currently syncing
470
- errors: Map<string, { error: string; retries: number }>;
471
- isPaused: boolean;
472
- isSyncing: boolean;
473
- };
474
457
  ```
475
458
 
476
459
  ## Best Practices
477
460
 
478
- 1. **Use Optimistic Updates**: Let users see changes immediately while syncing in the background
479
- 2. **Handle Errors Gracefully**: Show error states and provide retry mechanisms
480
- 3. **Configure Debouncing**: Adjust `syncDebounce` based on your use case
481
- 4. **Leverage Context**: Use context for filters, pagination, and search
482
- 5. **Monitor Sync Queue**: Display pending changes and errors to users
483
- 6. **Cache Wisely**: Configure `cacheTtl` and `cacheCapacity` based on your data freshness requirements
461
+ 1. **Use Optimistic Updates** - Let users see changes immediately while syncing in the background
462
+ 2. **Handle Errors Gracefully** - Show error states and provide retry mechanisms
463
+ 3. **Leverage Context** - Use context for filters, pagination, and search to trigger automatic refetches
464
+ 4. **Use getItem() + useItem()** - For individual item management with automatic React optimization
465
+ 5. **Monitor Sync Queue** - Display pending changes and errors to users for transparency
466
+ 6. **Use createSyncClient/Server** - For end-to-end type-safe CRUD with minimal boilerplate
484
467
 
485
468
  ## Architecture
486
469
 
487
470
  The library is built on several core concepts:
488
471
 
489
- - **Collection**: Manages the item collection, sync queue, and fetch handler
490
- - **SyncQueue**: Handles debounced synchronization with retry logic
491
- - **FetchHandler**: Manages data fetching with caching
492
- - **Item**: Represents individual items with their sync state
472
+ - **Collection** - Manages the item collection, sync queue, and fetch handler
473
+ - **SyncQueue** - Handles debounced synchronization with retry logic
474
+ - **FetchHandler** - Manages data fetching with caching
475
+ - **Item** - Represents individual items with WeakMap-based caching for React optimization
493
476
 
494
477
  All state updates use [Mutative](https://github.com/unadlib/mutative) for immutable updates, ensuring React can efficiently detect changes.
495
478
 
@@ -499,4 +482,4 @@ This is an alpha release. Please read the source code for a deeper understanding
499
482
 
500
483
  ## License
501
484
 
502
- MIT
485
+ MIT
@@ -11,9 +11,9 @@ export type CollectionState<T, C> = {
11
11
  fetchStatus: FetchState;
12
12
  fetchError?: string;
13
13
  };
14
- export declare class Collection<T, C> {
14
+ export declare class Collection<T extends object, C> {
15
15
  private static _cache;
16
- static get<T, C>(config: Config<T, C>): Collection<T, C>;
16
+ static get<T extends object, C>(config: Config<T, C>): Collection<T, C>;
17
17
  static clear(id: string): void;
18
18
  static clearAll(): void;
19
19
  readonly id: string;
@@ -38,7 +38,6 @@ export declare class Collection<T, C> {
38
38
  update(id: string, mutate: (draft: Draft<T>) => void): void;
39
39
  remove(id: string): void;
40
40
  getItem(id: string): Item<T, C>;
41
- _releaseItem(id: string): void;
42
41
  setContext(patchContext: Mutator<C>): void;
43
42
  refresh(): Promise<void>;
44
43
  pauseSync(): void;