@asaidimu/react-store 3.0.0 → 5.0.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 +371 -264
- package/index.cjs +1 -1
- package/index.d.cts +156 -30
- package/index.d.ts +156 -30
- package/index.js +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# @asaidimu/react-store
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@asaidimu/react-store)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.npmjs.com/package/@asaidimu/react-store)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/asaidimu/node-react/actions)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
6
7
|
|
|
7
|
-
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware system.
|
|
8
|
+
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware and artifact management system.
|
|
8
9
|
|
|
9
10
|
⚠️ **Beta Warning**
|
|
10
11
|
This package is currently in **beta**. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
|
|
@@ -20,10 +21,10 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
20
21
|
* [Using in Components](#using-in-components)
|
|
21
22
|
* [Handling Deletions](#handling-deletions)
|
|
22
23
|
* [Persistence](#persistence)
|
|
23
|
-
* [Middleware](#middleware)
|
|
24
|
+
* [Middleware (Transform & Validate)](#middleware-transform--validate)
|
|
25
|
+
* [Artifact Management](#artifact-management)
|
|
24
26
|
* [Observability](#observability)
|
|
25
27
|
* [Remote Observability](#remote-observability)
|
|
26
|
-
* [Transaction Support](#transaction-support)
|
|
27
28
|
* [Event System](#event-system)
|
|
28
29
|
* [Watching Action Loading States](#watching-action-loading-states)
|
|
29
30
|
* [Advanced Hook Properties](#advanced-hook-properties)
|
|
@@ -43,20 +44,20 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
43
44
|
|
|
44
45
|
## Overview & Features
|
|
45
46
|
|
|
46
|
-
`@asaidimu/react-store` provides an efficient and predictable way to manage complex application state in React applications. It goes beyond basic state management by integrating features typically found in separate libraries, such as data persistence and comprehensive observability tools, directly into its core. This allows developers to build robust, high-performance applications with deep insights into state changes and application behavior.
|
|
47
|
+
`@asaidimu/react-store` provides an efficient and predictable way to manage complex application state in React applications. It goes beyond basic state management by integrating features typically found in separate libraries, such as artifact management, data persistence, and comprehensive observability tools, directly into its core. This allows developers to build robust, high-performance applications with deep insights into state changes and application behavior.
|
|
47
48
|
|
|
48
|
-
Designed with modern React in mind, it leverages `useSyncExternalStore` for optimal performance and reactivity, ensuring components re-render only when relevant parts of the state change. Its flexible design supports a variety of use cases, from simple counter applications to complex data flows requiring atomic updates and cross-tab synchronization. The library is built with TypeScript from the ground up, offering strong type safety throughout your application's state and
|
|
49
|
+
Designed with modern React in mind, it leverages `useSyncExternalStore` for optimal performance and reactivity, ensuring components re-render only when relevant parts of the state change. Its flexible design supports a variety of use cases, from simple counter applications to complex data flows requiring atomic updates and cross-tab synchronization. The library is built with TypeScript from the ground up, offering strong type safety throughout your application's state, actions, and resolved artifacts.
|
|
49
50
|
|
|
50
51
|
### Key Features
|
|
51
52
|
|
|
52
53
|
* 📊 **Reactive State Management**: Automatically tracks dependencies to optimize component renders and ensure efficient updates using `useSyncExternalStore`.
|
|
53
|
-
* 🛡️ **Type-Safe**: Developed entirely in TypeScript, providing strict type checking for state, actions, and middleware.
|
|
54
|
-
* ⚙️ **Middleware Pipeline**: Implement custom logic to transform
|
|
55
|
-
* 📦 **Transaction Support**: Group multiple state updates into a single atomic operation, with automatic rollback if any part of the transaction fails.
|
|
54
|
+
* 🛡️ **Type-Safe**: Developed entirely in TypeScript, providing strict type checking for state, actions, artifacts, and middleware.
|
|
55
|
+
* ⚙️ **Middleware Pipeline**: Implement custom logic to `transform` or `validate` state changes before they are applied.
|
|
56
56
|
* 💾 **Built-in Persistence**: Seamlessly integrate with web storage mechanisms like `IndexedDB` and `WebStorage` (localStorage/sessionStorage), including cross-tab synchronization.
|
|
57
|
-
* 🔍 **Deep Observability**: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities via the `
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* 🔍 **Deep Observability**: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities via the `StoreObserver` instance.
|
|
58
|
+
* 🚀 **Artifact Management**: Define and reactively resolve asynchronous resources, services, or derived data, enabling advanced dependency injection patterns and lazy loading of complex logic.
|
|
59
|
+
* ⚡ **Performance Optimized**: Features intelligent selector caching and debounced actions with configurable immediate execution to prevent rapid successive calls and ensure smooth application performance.
|
|
60
|
+
* ⏱️ **Action Loading States**: Track the real-time loading status of individual actions, providing immediate feedback on asynchronous operations.
|
|
60
61
|
* ⚛️ **React 19+ Ready**: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
|
|
61
62
|
* 🗑️ **Explicit Deletions**: Use `Symbol.for("delete")` to explicitly remove properties from nested state objects.
|
|
62
63
|
|
|
@@ -73,7 +74,7 @@ Designed with modern React in mind, it leverages `useSyncExternalStore` for opti
|
|
|
73
74
|
To add `@asaidimu/react-store` to your project, run one of the following commands:
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
|
-
bun
|
|
77
|
+
bun add @asaidimu/react-store
|
|
77
78
|
# or
|
|
78
79
|
npm install @asaidimu/react-store
|
|
79
80
|
# or
|
|
@@ -82,7 +83,7 @@ yarn add @asaidimu/react-store
|
|
|
82
83
|
|
|
83
84
|
### Configuration
|
|
84
85
|
|
|
85
|
-
No global configuration is required. All options are passed during store creation via the `createStore` function. Configuration includes enabling metrics,
|
|
86
|
+
No global configuration is required. All options are passed during store creation via the `createStore` function. Configuration includes enabling metrics, persistence, and performance thresholds.
|
|
86
87
|
|
|
87
88
|
### Verification
|
|
88
89
|
|
|
@@ -93,35 +94,45 @@ import { createStore } from '@asaidimu/react-store';
|
|
|
93
94
|
|
|
94
95
|
interface MyState {
|
|
95
96
|
value: string;
|
|
97
|
+
count: number;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
const
|
|
99
|
-
state: { value: 'hello' },
|
|
100
|
+
const useMyStore = createStore<MyState, any, any>({
|
|
101
|
+
state: { value: 'hello', count: 0 },
|
|
100
102
|
actions: {
|
|
101
103
|
setValue: (_, newValue: string) => ({ value: newValue }),
|
|
104
|
+
increment: ({ state }) => ({ count: state.count + 1 }),
|
|
102
105
|
},
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
|
|
108
|
+
function MyComponent() {
|
|
109
|
+
const { select, actions } = useMyStore(); // Instantiate the hook
|
|
110
|
+
const currentValue = select(s => s.value);
|
|
111
|
+
const currentCount = select(s => s.count);
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
return (
|
|
114
|
+
<div>
|
|
115
|
+
<p>Value: {currentValue}</p>
|
|
116
|
+
<p>Count: {currentCount}</p>
|
|
117
|
+
<button onClick={() => actions.setValue('world')}>Set Value to 'world'</button>
|
|
118
|
+
<button onClick={() => actions.increment()}>Increment Count</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
110
122
|
|
|
111
|
-
//
|
|
112
|
-
|
|
123
|
+
// Render MyComponent in your React app.
|
|
124
|
+
// If no errors are thrown during installation or when running this basic example,
|
|
125
|
+
// the package is correctly installed and configured.
|
|
113
126
|
```
|
|
114
127
|
|
|
115
|
-
If no errors are thrown during installation or when running this basic example, the package is correctly installed and configured.
|
|
116
|
-
|
|
117
128
|
## Usage Documentation
|
|
118
129
|
|
|
119
130
|
### Creating a Store
|
|
120
131
|
|
|
121
|
-
Define your application state and
|
|
132
|
+
Define your application state, actions, and optionally artifacts, then create a store using `createStore`. The `actions` object maps action names to functions that receive an `ActionContext` (containing the current `state` and a `resolve` function for artifacts) and any additional arguments.
|
|
122
133
|
|
|
123
134
|
```tsx
|
|
124
|
-
// ui/store.tsx (Example
|
|
135
|
+
// ui/store.tsx (Example)
|
|
125
136
|
import { createStore } from '@asaidimu/react-store'; // Assuming direct import or wrapper
|
|
126
137
|
|
|
127
138
|
export interface Product {
|
|
@@ -149,6 +160,8 @@ export interface ECommerceState extends Record<string, any>{
|
|
|
149
160
|
orders: Order[];
|
|
150
161
|
topSellers: { id: number; name: string; sales: number }[];
|
|
151
162
|
activeUsers: number;
|
|
163
|
+
// A property to demonstrate artifact dependency
|
|
164
|
+
currency: string;
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
const initialState: ECommerceState = {
|
|
@@ -169,6 +182,7 @@ const initialState: ECommerceState = {
|
|
|
169
182
|
{ id: 4, name: 'Webcam', sales: 65 },
|
|
170
183
|
],
|
|
171
184
|
activeUsers: 1428,
|
|
185
|
+
currency: 'USD',
|
|
172
186
|
};
|
|
173
187
|
|
|
174
188
|
const actions = {
|
|
@@ -212,7 +226,6 @@ const actions = {
|
|
|
212
226
|
}).sort((a, b) => b.sales - a.sales),
|
|
213
227
|
};
|
|
214
228
|
},
|
|
215
|
-
|
|
216
229
|
updateStock: ({state}: {state:ECommerceState}) => ({
|
|
217
230
|
products: state.products.map((p:any) => ({
|
|
218
231
|
...p,
|
|
@@ -235,12 +248,40 @@ const actions = {
|
|
|
235
248
|
orders: [newOrder, ...state.orders],
|
|
236
249
|
};
|
|
237
250
|
},
|
|
251
|
+
setCurrency: ({state}, newCurrency: string) => ({ currency: newCurrency }),
|
|
238
252
|
};
|
|
239
253
|
|
|
240
254
|
export const useStore = createStore(
|
|
241
255
|
{
|
|
242
256
|
state: initialState,
|
|
243
257
|
actions,
|
|
258
|
+
// Example artifact definition
|
|
259
|
+
artifacts: {
|
|
260
|
+
currencySymbol: {
|
|
261
|
+
factory: async ({ use }) => {
|
|
262
|
+
const currency = await use(({ select }) => select((s: ECommerceState) => s.currency));
|
|
263
|
+
switch (currency) {
|
|
264
|
+
case 'USD': return '$';
|
|
265
|
+
case 'EUR': return '€';
|
|
266
|
+
case 'GBP': return '£';
|
|
267
|
+
default: return currency;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
// Another artifact example, might depend on other artifacts or state
|
|
272
|
+
exchangeRate: {
|
|
273
|
+
factory: async ({ resolve, use }) => {
|
|
274
|
+
const baseCurrency = await use(({ select }) => select((s: ECommerceState) => s.currency));
|
|
275
|
+
const targetCurrency = 'EUR'; // For demonstration
|
|
276
|
+
// In a real app, this would fetch from an API
|
|
277
|
+
await new Promise(r => setTimeout(r, 50)); // Simulate API delay
|
|
278
|
+
if (baseCurrency === 'USD' && targetCurrency === 'EUR') {
|
|
279
|
+
return 0.92; // 1 USD = 0.92 EUR
|
|
280
|
+
}
|
|
281
|
+
return 1.0;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
244
285
|
},
|
|
245
286
|
{ enableMetrics: true } // Enables metrics for observability
|
|
246
287
|
);
|
|
@@ -251,16 +292,16 @@ export const useStore = createStore(
|
|
|
251
292
|
Consume your store's state and actions within your React components using the exported hook. The `select` function allows you to subscribe to specific parts of the state, ensuring that your components only re-render when the selected data changes.
|
|
252
293
|
|
|
253
294
|
```tsx
|
|
254
|
-
// ui/App.tsx (Excerpt
|
|
295
|
+
// ui/App.tsx (Excerpt)
|
|
255
296
|
import { useEffect, useMemo } from 'react';
|
|
256
297
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
257
|
-
import { useStore, Product, CartItem, Order } from './store';
|
|
298
|
+
import { useStore, Product, CartItem, Order, ECommerceState } from './store';
|
|
258
299
|
|
|
259
|
-
// ... (ShadCN-like UI Components for brevity) ...
|
|
260
300
|
|
|
261
301
|
const ProductCatalog = () => {
|
|
262
|
-
const { select, actions } = useStore();
|
|
302
|
+
const { select, actions, resolve } = useStore();
|
|
263
303
|
const products = select((state) => state.products); // Granular selection
|
|
304
|
+
const { instance: currencySymbol, ready: currencyReady } = resolve('currencySymbol'); // Reactive artifact resolution
|
|
264
305
|
|
|
265
306
|
return (
|
|
266
307
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
@@ -272,7 +313,9 @@ const ProductCatalog = () => {
|
|
|
272
313
|
<CardContent className="flex-grow">
|
|
273
314
|
<img src={product.image} alt={product.name} className="w-full h-40 object-cover rounded-lg mb-4" />
|
|
274
315
|
<div className="flex justify-between items-center">
|
|
275
|
-
<p className="text-lg font-semibold text-gray-700"
|
|
316
|
+
<p className="text-lg font-semibold text-gray-700">
|
|
317
|
+
{currencyReady ? currencySymbol : ''}{product.price.toFixed(2)}
|
|
318
|
+
</p>
|
|
276
319
|
<p className="text-sm text-gray-500">{product.stock} in stock</p>
|
|
277
320
|
</div>
|
|
278
321
|
</CardContent>
|
|
@@ -286,7 +329,8 @@ const ProductCatalog = () => {
|
|
|
286
329
|
};
|
|
287
330
|
|
|
288
331
|
function App() {
|
|
289
|
-
const { actions } = useStore();
|
|
332
|
+
const { actions, select } = useStore();
|
|
333
|
+
const currentCurrency = select((state) => state.currency);
|
|
290
334
|
|
|
291
335
|
useEffect(() => {
|
|
292
336
|
// Real-time simulations for the dashboard
|
|
@@ -299,26 +343,35 @@ function App() {
|
|
|
299
343
|
clearInterval(usersInterval);
|
|
300
344
|
clearInterval(ordersInterval);
|
|
301
345
|
};
|
|
302
|
-
}, [actions]);
|
|
346
|
+
}, [actions]);
|
|
303
347
|
|
|
304
348
|
return (
|
|
305
349
|
<div className="bg-gray-50 text-gray-900 min-h-screen">
|
|
306
350
|
<header className="border-b">
|
|
307
|
-
<div className="container mx-auto px-4 h-16 flex items-center">
|
|
351
|
+
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
|
308
352
|
<h1 className="text-xl font-bold">E-Commerce Dashboard</h1>
|
|
353
|
+
<select
|
|
354
|
+
value={currentCurrency}
|
|
355
|
+
onChange={(e) => actions.setCurrency(e.target.value)}
|
|
356
|
+
className="p-2 border rounded-md"
|
|
357
|
+
>
|
|
358
|
+
<option value="USD">USD</option>
|
|
359
|
+
<option value="EUR">EUR</option>
|
|
360
|
+
<option value="GBP">GBP</option>
|
|
361
|
+
</select>
|
|
309
362
|
</div>
|
|
310
363
|
</header>
|
|
311
364
|
<main className="container mx-auto p-4 sm:p-6 lg:p-8">
|
|
312
365
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
313
366
|
<div className="lg:col-span-2 space-y-6">
|
|
314
367
|
<ProductCatalog />
|
|
315
|
-
<LiveInventoryChart />
|
|
368
|
+
{/* <LiveInventoryChart /> */}
|
|
316
369
|
</div>
|
|
317
370
|
<div className="space-y-6">
|
|
318
|
-
<ShoppingCart />
|
|
319
|
-
<UserAnalytics />
|
|
320
|
-
<TopSellingProducts />
|
|
321
|
-
<RecentOrdersFeed />
|
|
371
|
+
{/* <ShoppingCart /> */}
|
|
372
|
+
{/* <UserAnalytics /> */}
|
|
373
|
+
{/* <TopSellingProducts /> */}
|
|
374
|
+
{/* <RecentOrdersFeed /> */}
|
|
322
375
|
</div>
|
|
323
376
|
</div>
|
|
324
377
|
</main>
|
|
@@ -395,12 +448,16 @@ Persist your store's state across browser sessions or synchronize it across mult
|
|
|
395
448
|
|
|
396
449
|
```tsx
|
|
397
450
|
import { createStore } from '@asaidimu/react-store';
|
|
398
|
-
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
|
|
451
|
+
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
|
|
399
452
|
import React, { useEffect } from 'react';
|
|
400
453
|
|
|
454
|
+
interface LocalState { sessionCount: number; lastVisited: string; }
|
|
455
|
+
interface SessionState { tabSpecificData: string; }
|
|
456
|
+
interface UserProfileState { userId: string; preferences: { language: string; darkMode: boolean; }; }
|
|
457
|
+
|
|
401
458
|
// 1. Using WebStoragePersistence (localStorage by default)
|
|
402
459
|
// Data persists even if the browser tab is closed and reopened.
|
|
403
|
-
const localStorePersistence = new WebStoragePersistence('my-app-state-key');
|
|
460
|
+
const localStorePersistence = new WebStoragePersistence<LocalState>('my-app-state-key');
|
|
404
461
|
const useLocalStore = createStore(
|
|
405
462
|
{
|
|
406
463
|
state: { sessionCount: 0, lastVisited: new Date().toISOString() },
|
|
@@ -414,12 +471,12 @@ const useLocalStore = createStore(
|
|
|
414
471
|
|
|
415
472
|
// 2. Using WebStoragePersistence (sessionStorage)
|
|
416
473
|
// Data only persists for the duration of the browser tab. Clears on tab close.
|
|
417
|
-
const sessionStoragePersistence = new WebStoragePersistence('my-session-state-key', true);
|
|
474
|
+
const sessionStoragePersistence = new WebStoragePersistence<SessionState>('my-session-state-key', true);
|
|
418
475
|
const useSessionStore = createStore(
|
|
419
476
|
{
|
|
420
477
|
state: { tabSpecificData: 'initial' },
|
|
421
478
|
actions: {
|
|
422
|
-
updateTabSpecificData: (
|
|
479
|
+
updateTabSpecificData: (_, newData: string) => ({ tabSpecificData: newData }),
|
|
423
480
|
},
|
|
424
481
|
},
|
|
425
482
|
{ persistence: sessionStoragePersistence },
|
|
@@ -427,12 +484,12 @@ const useSessionStore = createStore(
|
|
|
427
484
|
|
|
428
485
|
// 3. Using IndexedDBPersistence
|
|
429
486
|
// Ideal for larger amounts of data, offers robust cross-tab synchronization.
|
|
430
|
-
const indexedDBPersistence = new IndexedDBPersistence('user-profile-data');
|
|
487
|
+
const indexedDBPersistence = new IndexedDBPersistence<UserProfileState>('user-profile-data');
|
|
431
488
|
const useUserProfileStore = createStore(
|
|
432
489
|
{
|
|
433
490
|
state: { userId: '', preferences: { language: 'en', darkMode: false } },
|
|
434
491
|
actions: {
|
|
435
|
-
setUserId: (
|
|
492
|
+
setUserId: (_, id: string) => ({ userId: id }),
|
|
436
493
|
toggleDarkMode: ({state}) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
437
494
|
},
|
|
438
495
|
},
|
|
@@ -442,9 +499,13 @@ const useUserProfileStore = createStore(
|
|
|
442
499
|
function AppWithPersistence() {
|
|
443
500
|
const { select: selectLocal, actions: actionsLocal, isReady: localReady } = useLocalStore();
|
|
444
501
|
const { select: selectProfile, actions: actionsProfile, isReady: profileReady } = useUserProfileStore();
|
|
502
|
+
const { select: selectSession, actions: actionsSession } = useSessionStore();
|
|
503
|
+
|
|
445
504
|
|
|
446
505
|
const sessionCount = selectLocal(s => s.sessionCount);
|
|
447
506
|
const darkMode = selectProfile(s => s.preferences.darkMode);
|
|
507
|
+
const tabData = selectSession(s => s.tabSpecificData);
|
|
508
|
+
|
|
448
509
|
|
|
449
510
|
useEffect(() => {
|
|
450
511
|
if (localReady) {
|
|
@@ -454,7 +515,7 @@ function AppWithPersistence() {
|
|
|
454
515
|
if (profileReady && !selectProfile(s => s.userId)) {
|
|
455
516
|
actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
|
|
456
517
|
}
|
|
457
|
-
}, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]);
|
|
518
|
+
}, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]);
|
|
458
519
|
|
|
459
520
|
if (!localReady || !profileReady) {
|
|
460
521
|
return <div>Loading persisted data...</div>;
|
|
@@ -462,9 +523,15 @@ function AppWithPersistence() {
|
|
|
462
523
|
|
|
463
524
|
return (
|
|
464
525
|
<div>
|
|
465
|
-
<h3>Local Store</h3>
|
|
526
|
+
<h3>Local Store (localStorage)</h3>
|
|
466
527
|
<p>Session Count: {sessionCount}</p>
|
|
467
528
|
|
|
529
|
+
<h3>Session Store (sessionStorage)</h3>
|
|
530
|
+
<p>Tab Specific Data: {tabData}</p>
|
|
531
|
+
<button onClick={() => actionsSession.updateTabSpecificData('updated-for-this-tab')}>
|
|
532
|
+
Update Tab Data
|
|
533
|
+
</button>
|
|
534
|
+
|
|
468
535
|
<h3>User Profile Store (IndexedDB)</h3>
|
|
469
536
|
<p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
|
|
470
537
|
<button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
|
|
@@ -473,12 +540,15 @@ function AppWithPersistence() {
|
|
|
473
540
|
}
|
|
474
541
|
```
|
|
475
542
|
|
|
476
|
-
### Middleware
|
|
543
|
+
### Middleware (Transform & Validate)
|
|
544
|
+
|
|
545
|
+
Middleware functions can intercept and modify or block state updates. The `CHANGELOG.md` indicates a breaking change, moving from generic `middleware` and `blockingMiddleware` to `transform` and `validate` properties in the `StoreDefinition`. These now receive an `ActionContext` with `state` and `resolve` capabilities.
|
|
477
546
|
|
|
478
|
-
|
|
547
|
+
* `transform`: Functions that run *after* an action's core logic but *before* the state update is committed. They can modify the `DeepPartial<TState>` that will be merged into the state.
|
|
548
|
+
* `validate`: Functions that run *before* the state update is committed. If a validator returns `false` (or a `Promise<false>`), the state update is entirely cancelled.
|
|
479
549
|
|
|
480
550
|
```typescript
|
|
481
|
-
import { createStore
|
|
551
|
+
import { createStore } from '@asaidimu/react-store';
|
|
482
552
|
import React from 'react';
|
|
483
553
|
|
|
484
554
|
interface CartState {
|
|
@@ -486,28 +556,7 @@ interface CartState {
|
|
|
486
556
|
total: number;
|
|
487
557
|
}
|
|
488
558
|
|
|
489
|
-
const
|
|
490
|
-
if (update.items) {
|
|
491
|
-
const newItems = update.items as CartState['items'];
|
|
492
|
-
const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
|
|
493
|
-
return { ...update, total: newTotal };
|
|
494
|
-
}
|
|
495
|
-
return update;
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) => {
|
|
499
|
-
if (update.items) {
|
|
500
|
-
for (const item of update.items as CartState['items']) {
|
|
501
|
-
if (item.quantity < 0) {
|
|
502
|
-
console.warn('Blocked: Item quantity cannot be negative.');
|
|
503
|
-
return false; // Blocks the update
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return true; // Allows the update
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const useCartStore = createStore({
|
|
559
|
+
const useCartStore = createStore<CartState, any, any>({ // TArtifactsMap and TActions are inferred
|
|
511
560
|
state: { items: [], total: 0 },
|
|
512
561
|
actions: {
|
|
513
562
|
addItem: ({state}, item: { id: string; name: string; price: number }) => {
|
|
@@ -527,9 +576,31 @@ const useCartStore = createStore({
|
|
|
527
576
|
items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
|
|
528
577
|
}),
|
|
529
578
|
},
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
579
|
+
transform: {
|
|
580
|
+
// Calculates total based on updated items before state merge
|
|
581
|
+
calculateTotal: async ({ state, resolve }, update) => {
|
|
582
|
+
if (update.items) {
|
|
583
|
+
const newItems = update.items as CartState['items'];
|
|
584
|
+
const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
|
|
585
|
+
return { ...update, total: newTotal };
|
|
586
|
+
}
|
|
587
|
+
return update;
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
validate: {
|
|
591
|
+
// Blocks update if any item quantity is negative
|
|
592
|
+
validateItemQuantity: async ({ state, resolve }, update) => {
|
|
593
|
+
if (update.items) {
|
|
594
|
+
for (const item of update.items as CartState['items']) {
|
|
595
|
+
if (item.quantity < 0) {
|
|
596
|
+
console.warn('Blocked by validator: Item quantity cannot be negative.');
|
|
597
|
+
return false; // Blocks the update
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return true; // Allows the update
|
|
602
|
+
},
|
|
603
|
+
},
|
|
533
604
|
});
|
|
534
605
|
|
|
535
606
|
function CartComponent() {
|
|
@@ -557,6 +628,124 @@ function CartComponent() {
|
|
|
557
628
|
}
|
|
558
629
|
```
|
|
559
630
|
|
|
631
|
+
### Artifact Management
|
|
632
|
+
|
|
633
|
+
The store supports defining and reactively resolving "artifacts," which can be any asynchronous resource, service, or derived value. Artifacts are defined in the `artifacts` property of the `StoreDefinition` and resolved using `ctx.resolve()` within actions or the `resolve()` hook in components. They can depend on other artifacts or on the store's reactive state.
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
import { createStore } from '@asaidimu/react-store';
|
|
637
|
+
import { ArtifactScopes } from '@asaidimu/utils-artifacts';
|
|
638
|
+
import React, { useEffect } from 'react';
|
|
639
|
+
|
|
640
|
+
interface AppState {
|
|
641
|
+
userId: string | null;
|
|
642
|
+
settingsLoaded: boolean;
|
|
643
|
+
theme: string;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Assume this is an API service or similar
|
|
647
|
+
const mockApiService = {
|
|
648
|
+
fetchUserSettings: async (userId: string) => {
|
|
649
|
+
await new Promise(r => setTimeout(r, 200)); // Simulate API delay
|
|
650
|
+
if (userId === 'user-123') {
|
|
651
|
+
return { theme: 'dark', notifications: true };
|
|
652
|
+
}
|
|
653
|
+
return { theme: 'light', notifications: false };
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const useArtifactStore = createStore<AppState, any, any>({
|
|
658
|
+
state: { userId: null, settingsLoaded: false, theme: 'light' },
|
|
659
|
+
actions: {
|
|
660
|
+
setUserId: (_, id: string) => ({ userId: id, settingsLoaded: false }),
|
|
661
|
+
loadUserSettings: async ({ state, resolve }) => {
|
|
662
|
+
if (!state.userId) return;
|
|
663
|
+
|
|
664
|
+
const { instance: userSettings } = await resolve('userSettings');
|
|
665
|
+
if (userSettings) {
|
|
666
|
+
return {
|
|
667
|
+
settingsLoaded: true,
|
|
668
|
+
theme: userSettings.theme,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {};
|
|
672
|
+
},
|
|
673
|
+
toggleTheme: ({state}) => ({ theme: state.theme === 'light' ? 'dark' : 'light' }),
|
|
674
|
+
},
|
|
675
|
+
artifacts: {
|
|
676
|
+
// A singleton artifact that fetches user settings based on the current userId in state
|
|
677
|
+
userSettings: {
|
|
678
|
+
scope: ArtifactScopes.Singleton, // Ensure only one instance is created globally
|
|
679
|
+
factory: async ({ use }) => {
|
|
680
|
+
const userId = await use(({ select }) => select((s: AppState) => s.userId));
|
|
681
|
+
if (userId) {
|
|
682
|
+
console.log(`Fetching settings for user: ${userId}`);
|
|
683
|
+
return mockApiService.fetchUserSettings(userId);
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
},
|
|
687
|
+
lazy: true, // Only create/resolve when first requested
|
|
688
|
+
},
|
|
689
|
+
// An artifact that provides a simple logger instance
|
|
690
|
+
logger: {
|
|
691
|
+
factory: async () => console,
|
|
692
|
+
scope: ArtifactScopes.Singleton,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
function ArtifactConsumer() {
|
|
698
|
+
const { actions, select, resolve, isReady } = useArtifactStore();
|
|
699
|
+
const userId = select(s => s.userId);
|
|
700
|
+
const theme = select(s => s.theme);
|
|
701
|
+
const settingsLoaded = select(s => s.settingsLoaded);
|
|
702
|
+
|
|
703
|
+
// Reactively resolve the userSettings artifact in the component
|
|
704
|
+
const { instance: userSettingsArtifact, ready: userSettingsReady } = resolve('userSettings');
|
|
705
|
+
const { instance: logger } = resolve('logger');
|
|
706
|
+
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
// Simulate setting a user ID after initial load
|
|
709
|
+
if (isReady && !userId) {
|
|
710
|
+
actions.setUserId('user-123');
|
|
711
|
+
}
|
|
712
|
+
}, [isReady, userId, actions]);
|
|
713
|
+
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
// Automatically load settings when userId is available and settings not loaded
|
|
716
|
+
if (userId && !settingsLoaded) {
|
|
717
|
+
actions.loadUserSettings();
|
|
718
|
+
}
|
|
719
|
+
}, [userId, settingsLoaded, actions]);
|
|
720
|
+
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (logger && userSettingsArtifact) {
|
|
723
|
+
logger.log("User settings artifact updated:", userSettingsArtifact);
|
|
724
|
+
}
|
|
725
|
+
}, [logger, userSettingsArtifact]);
|
|
726
|
+
|
|
727
|
+
if (!isReady) {
|
|
728
|
+
return <div>Loading store...</div>;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<div>
|
|
733
|
+
<h2>Artifact Management Example</h2>
|
|
734
|
+
<p>Current User ID: {userId || 'Not set'}</p>
|
|
735
|
+
<p>Settings Loaded: {settingsLoaded ? 'Yes' : 'No'}</p>
|
|
736
|
+
<p>Current Theme: {theme}</p>
|
|
737
|
+
{userSettingsReady && userSettingsArtifact && (
|
|
738
|
+
<p>Artifact (userSettings) Resolved Theme: {userSettingsArtifact.theme}</p>
|
|
739
|
+
)}
|
|
740
|
+
<button onClick={() => actions.setUserId(userId === 'user-123' ? 'user-456' : 'user-123')}>
|
|
741
|
+
Toggle User ID
|
|
742
|
+
</button>
|
|
743
|
+
<button onClick={() => actions.toggleTheme()}>Toggle App Theme</button>
|
|
744
|
+
</div>
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
560
749
|
### Observability
|
|
561
750
|
|
|
562
751
|
Enable metrics and debugging via the `observer` and `actionTracker` objects. The `enableMetrics` option in `createStore` is crucial for activating these features.
|
|
@@ -567,10 +756,15 @@ import React from 'react';
|
|
|
567
756
|
|
|
568
757
|
const useObservedStore = createStore(
|
|
569
758
|
{
|
|
570
|
-
state: { task: '', completed: false },
|
|
759
|
+
state: { task: '', completed: false, count: 0 },
|
|
571
760
|
actions: {
|
|
572
|
-
addTask: (
|
|
573
|
-
completeTask: (
|
|
761
|
+
addTask: (_, taskName: string) => ({ task: taskName, completed: false }),
|
|
762
|
+
completeTask: (_) => ({ completed: true }),
|
|
763
|
+
increment: ({state}) => ({ count: state.count + 1 }),
|
|
764
|
+
longRunningAction: async () => {
|
|
765
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async work
|
|
766
|
+
return { count: 100 };
|
|
767
|
+
},
|
|
574
768
|
},
|
|
575
769
|
},
|
|
576
770
|
{
|
|
@@ -588,7 +782,8 @@ const useObservedStore = createStore(
|
|
|
588
782
|
);
|
|
589
783
|
|
|
590
784
|
function DebugPanel() {
|
|
591
|
-
const { actions, observer, actionTracker } = useObservedStore();
|
|
785
|
+
const { actions, observer, actionTracker, select, state: getStateSnapshot } = useObservedStore();
|
|
786
|
+
const count = select(s => s.count);
|
|
592
787
|
|
|
593
788
|
// Access performance metrics
|
|
594
789
|
const metrics = observer?.getPerformanceMetrics();
|
|
@@ -610,9 +805,12 @@ function DebugPanel() {
|
|
|
610
805
|
<p>Largest Update Size (paths): {metrics?.largestUpdateSize}</p>
|
|
611
806
|
|
|
612
807
|
<h3>Time Travel</h3>
|
|
808
|
+
<p>Current Count: {count} (via select)</p>
|
|
613
809
|
<button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
|
|
614
810
|
<button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
|
|
615
811
|
<p>State History: {timeTravel?.getHistoryLength()}</p>
|
|
812
|
+
<p>Current Snapshot (non-reactive): {JSON.stringify(getStateSnapshot())}</p>
|
|
813
|
+
|
|
616
814
|
|
|
617
815
|
<h3>Action History</h3>
|
|
618
816
|
<ul>
|
|
@@ -626,6 +824,8 @@ function DebugPanel() {
|
|
|
626
824
|
)}
|
|
627
825
|
<button onClick={() => actions.addTask('Learn React Store')}>Add Task</button>
|
|
628
826
|
<button onClick={() => actions.completeTask()}>Complete Task</button>
|
|
827
|
+
<button onClick={() => actions.increment()}>Increment</button>
|
|
828
|
+
<button onClick={() => actions.longRunningAction()}>Long Action</button>
|
|
629
829
|
</div>
|
|
630
830
|
);
|
|
631
831
|
}
|
|
@@ -637,7 +837,8 @@ Send collected metrics and traces to external systems like OpenTelemetry, Promet
|
|
|
637
837
|
|
|
638
838
|
```tsx
|
|
639
839
|
import { createStore } from '@asaidimu/react-store';
|
|
640
|
-
|
|
840
|
+
// Assuming useRemoteObservability is provided by @asaidimu/utils-store or a wrapper
|
|
841
|
+
// import { useRemoteObservability } from '@asaidimu/utils-store';
|
|
641
842
|
import React, { useEffect } from 'react';
|
|
642
843
|
|
|
643
844
|
const useRemoteStore = createStore(
|
|
@@ -650,64 +851,48 @@ const useRemoteStore = createStore(
|
|
|
650
851
|
}
|
|
651
852
|
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
|
|
652
853
|
},
|
|
653
|
-
handleApiError: (
|
|
854
|
+
handleApiError: (_, error: string) => ({ lastApiError: error })
|
|
654
855
|
},
|
|
655
856
|
},
|
|
656
857
|
{
|
|
657
858
|
enableMetrics: true, // Required for RemoteObservability
|
|
658
859
|
enableConsoleLogging: false,
|
|
659
|
-
// collectCategories configuration would typically be passed to useRemoteObservability
|
|
660
|
-
// reportingInterval, batchSize, immediateReporting would also be part of useRemoteObservability options
|
|
661
860
|
}
|
|
662
861
|
);
|
|
663
862
|
|
|
664
863
|
function MonitoringIntegration() {
|
|
665
864
|
const { store, observer } = useRemoteStore();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
865
|
+
|
|
866
|
+
// Placeholder for actual useRemoteObservability hook
|
|
867
|
+
// const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
|
|
868
|
+
// serviceName: 'my-react-app',
|
|
869
|
+
// environment: 'development',
|
|
870
|
+
// instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
|
|
871
|
+
// collectCategories: {
|
|
872
|
+
// performance: true,
|
|
873
|
+
// errors: true,
|
|
874
|
+
// stateChanges: true,
|
|
875
|
+
// middleware: true,
|
|
876
|
+
// },
|
|
877
|
+
// reportingInterval: 10000, // Send metrics every 10 seconds
|
|
878
|
+
// batchSize: 10, // Send after 10 metrics or interval, whichever comes first
|
|
879
|
+
// immediateReporting: false, // Don't send immediately after each metric
|
|
880
|
+
// });
|
|
681
881
|
|
|
682
882
|
useEffect(() => {
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
// Add Prometheus Pushgateway as a destination
|
|
691
|
-
addPrometheusDestination({
|
|
692
|
-
pushgatewayUrl: 'http://localhost:9091', // Default Prometheus Pushgateway
|
|
693
|
-
jobName: 'react-store-metrics',
|
|
694
|
-
username: 'promuser', // Optional basic auth
|
|
695
|
-
password: 'prompassword',
|
|
696
|
-
});
|
|
883
|
+
// In a real implementation, you would use the `remote` object
|
|
884
|
+
// to add destinations and configure reporting.
|
|
885
|
+
// Example:
|
|
886
|
+
// addOpenTelemetryDestination({ endpoint: 'http://localhost:4318', apiKey: 'your-otel-api-key' });
|
|
887
|
+
// addPrometheusDestination({ pushgatewayUrl: 'http://localhost:9091', jobName: 'react-store-metrics' });
|
|
888
|
+
// addGrafanaCloudDestination({ url: 'https://loki-prod-us-central1.grafana.net', apiKey: 'your-grafana-cloud-api-key' });
|
|
697
889
|
|
|
698
|
-
// Add Grafana Cloud Loki as a destination (for logs/traces)
|
|
699
|
-
addGrafanaCloudDestination({
|
|
700
|
-
url: 'https://loki-prod-us-central1.grafana.net', // Example Loki endpoint
|
|
701
|
-
apiKey: 'your-grafana-cloud-api-key',
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Report current store metrics periodically (in addition to event-driven metrics)
|
|
705
890
|
const interval = setInterval(() => {
|
|
706
|
-
observer?.reportCurrentMetrics();
|
|
891
|
+
observer?.reportCurrentMetrics(); // Manually trigger a report if needed
|
|
707
892
|
}, 5000); // Report every 5 seconds
|
|
708
893
|
|
|
709
894
|
return () => clearInterval(interval);
|
|
710
|
-
}, [observer
|
|
895
|
+
}, [observer]); // Removed placeholder dependencies for actual usage
|
|
711
896
|
|
|
712
897
|
return null; // This component doesn't render anything visually
|
|
713
898
|
}
|
|
@@ -725,85 +910,6 @@ function MonitoringIntegration() {
|
|
|
725
910
|
// }
|
|
726
911
|
```
|
|
727
912
|
|
|
728
|
-
### Transaction Support
|
|
729
|
-
|
|
730
|
-
Group related updates that should succeed or fail together. If an error occurs within the transaction, all changes made during that transaction are automatically rolled back, maintaining state consistency.
|
|
731
|
-
|
|
732
|
-
```typescript
|
|
733
|
-
import { createStore } from '@asaidimu/react-store';
|
|
734
|
-
import React from 'react';
|
|
735
|
-
|
|
736
|
-
interface BankState {
|
|
737
|
-
checking: number;
|
|
738
|
-
savings: number;
|
|
739
|
-
transactions: string[];
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const useBankStore = createStore<BankState, any>({
|
|
743
|
-
state: { checking: 1000, savings: 500, transactions: [] },
|
|
744
|
-
actions: {
|
|
745
|
-
transferFunds: async ({state}, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
746
|
-
if (amount <= 0) {
|
|
747
|
-
throw new Error('Transfer amount must be positive.');
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const newChecking = fromAccount === 'checking' ? state.checking - amount : state.checking + amount;
|
|
751
|
-
const newSavings = fromAccount === 'savings' ? state.savings - amount : state.savings + amount;
|
|
752
|
-
|
|
753
|
-
if ((fromAccount === 'checking' && newChecking < 0) || (fromAccount === 'savings' && newSavings < 0)) {
|
|
754
|
-
throw new Error('Insufficient funds.');
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Simulate a complex operation that might fail
|
|
758
|
-
if (amount > 700 && fromAccount === 'checking') {
|
|
759
|
-
throw new Error('Large transfers from checking require additional verification.');
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const newTransactions = [...state.transactions, `Transfer ${amount} from ${fromAccount} to ${toAccount}`];
|
|
763
|
-
return {
|
|
764
|
-
checking: newChecking,
|
|
765
|
-
savings: newSavings,
|
|
766
|
-
transactions: newTransactions,
|
|
767
|
-
};
|
|
768
|
-
},
|
|
769
|
-
},
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
function BankApp() {
|
|
773
|
-
const { select, actions, store } = useBankStore();
|
|
774
|
-
const checkingBalance = select(s => s.checking);
|
|
775
|
-
const savingsBalance = select(s => s.savings);
|
|
776
|
-
const transactions = select(s => s.transactions);
|
|
777
|
-
|
|
778
|
-
const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
|
|
779
|
-
try {
|
|
780
|
-
// Wrap the action call in a store transaction
|
|
781
|
-
await store.transaction(() => actions.transferFunds(from, to, amount));
|
|
782
|
-
alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
|
|
783
|
-
} catch (error) {
|
|
784
|
-
alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
785
|
-
// State is automatically rolled back if an error occurs within the transaction
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
return (
|
|
790
|
-
<div>
|
|
791
|
-
<h2>Bank Accounts</h2>
|
|
792
|
-
<p>Checking: ${checkingBalance.toFixed(2)}</p>
|
|
793
|
-
<p>Savings: ${savingsBalance.toFixed(2)}</p>
|
|
794
|
-
<h3>Recent Transactions</h3>
|
|
795
|
-
<ul>
|
|
796
|
-
{transactions.map((t, i) => <li key={i}>{t}</li>)}
|
|
797
|
-
</ul>
|
|
798
|
-
<button onClick={() => handleTransfer('checking', 'savings', 100)}>Transfer $100 (Checking to Savings)</button>
|
|
799
|
-
<button onClick={() => handleTransfer('savings', 'checking', 200)}>Transfer $200 (Savings to Checking)</button>
|
|
800
|
-
<button onClick={() => handleTransfer('checking', 'savings', 800)}>Transfer $800 (Will Fail)</button>
|
|
801
|
-
<button onClick={() => handleTransfer('checking', 'savings', 1500)}>Transfer $1500 (Insufficient Funds)</button>
|
|
802
|
-
</div>
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
```
|
|
806
|
-
|
|
807
913
|
### Event System
|
|
808
914
|
|
|
809
915
|
The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects. This is done via `store.onStoreEvent()`.
|
|
@@ -819,8 +925,8 @@ const useEventStore = createStore(
|
|
|
819
925
|
processData: ({state}, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
820
926
|
triggerError: () => { throw new Error("Action failed intentionally"); }
|
|
821
927
|
},
|
|
822
|
-
|
|
823
|
-
myLoggingMiddleware: (state, update) => {
|
|
928
|
+
transform: { // Using the new middleware API
|
|
929
|
+
myLoggingMiddleware: async ({state}, update) => {
|
|
824
930
|
console.log('Middleware processing:', update);
|
|
825
931
|
return update;
|
|
826
932
|
}
|
|
@@ -913,7 +1019,7 @@ interface DataState {
|
|
|
913
1019
|
const useDataStore = createStore({
|
|
914
1020
|
state: { items: [] },
|
|
915
1021
|
actions: {
|
|
916
|
-
fetchItems: async (state) => {
|
|
1022
|
+
fetchItems: async ({state}) => {
|
|
917
1023
|
// Simulate an API call
|
|
918
1024
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
919
1025
|
return { items: ['Item A', 'Item B', 'Item C'] };
|
|
@@ -960,7 +1066,7 @@ function DataLoader() {
|
|
|
960
1066
|
The hook returned by `createStore` provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
|
|
961
1067
|
|
|
962
1068
|
```tsx
|
|
963
|
-
import { useStore as useMyStore } from '
|
|
1069
|
+
import { useStore as useMyStore } from './store'; // Assuming this is your store definition
|
|
964
1070
|
|
|
965
1071
|
function MyAdvancedComponent() {
|
|
966
1072
|
const {
|
|
@@ -968,11 +1074,11 @@ function MyAdvancedComponent() {
|
|
|
968
1074
|
actions, // Object containing your defined actions (debounced, promise-returning)
|
|
969
1075
|
isReady, // Boolean indicating if persistence is ready
|
|
970
1076
|
store, // Direct access to the ReactiveDataStore instance (from @asaidimu/utils-store)
|
|
971
|
-
observer, //
|
|
1077
|
+
observer, // StoreObserver instance (from @asaidimu/utils-store, if `enableMetrics` was true)
|
|
972
1078
|
actionTracker, // Instance of ActionTracker for monitoring action executions (if `enableMetrics` was true)
|
|
973
1079
|
state, // A getter function `() => TState` to get the entire current state object (reactive)
|
|
974
1080
|
watch, // Function to watch the loading status of actions
|
|
975
|
-
resolve, // Function to resolve an artifact (if artifacts are defined)
|
|
1081
|
+
resolve, // Function to reactively resolve an artifact (if artifacts are defined)
|
|
976
1082
|
} = useMyStore(); // Assuming useMyStore is defined from createStore
|
|
977
1083
|
|
|
978
1084
|
// Example: Accessing the full state (use with caution for performance; `select` is preferred)
|
|
@@ -991,14 +1097,14 @@ function MyAdvancedComponent() {
|
|
|
991
1097
|
}
|
|
992
1098
|
|
|
993
1099
|
// Example: Watching a specific action's loading state
|
|
994
|
-
const isLoadingSomeAction = watch('checkout'); // Assuming 'checkout' is an action
|
|
1100
|
+
const isLoadingSomeAction = watch('checkout'); // Assuming 'checkout' is an action from the example store
|
|
995
1101
|
console.log("Is 'checkout' action loading?", isLoadingSomeAction);
|
|
996
1102
|
|
|
997
|
-
// Example: Resolving an artifact (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1103
|
+
// Example: Resolving an artifact (from the example store)
|
|
1104
|
+
const { instance: currencySymbol, ready: isCurrencySymbolReady } = resolve('currencySymbol');
|
|
1105
|
+
if (isCurrencySymbolReady) {
|
|
1106
|
+
console.log("Currency Symbol artifact is ready:", currencySymbol);
|
|
1107
|
+
}
|
|
1002
1108
|
|
|
1003
1109
|
return (
|
|
1004
1110
|
<div>
|
|
@@ -1017,53 +1123,50 @@ function MyAdvancedComponent() {
|
|
|
1017
1123
|
|
|
1018
1124
|
The core logic of this package integrates and extends external utility packages, with `src/store.ts` serving as the main entry point for the React hook.
|
|
1019
1125
|
|
|
1020
|
-
|
|
1021
|
-
.
|
|
1022
|
-
|
|
1023
|
-
│ ├── execution.ts # Manages tracking and history of action executions.
|
|
1024
|
-
│ ├── store.ts # The main `createStore` function, integrating `ReactiveDataStore` and `StoreObservability` from `@asaidimu/utils-store` with React's `useSyncExternalStore`.
|
|
1025
|
-
│ └── types.ts # Defines core TypeScript interfaces for store definitions, actions, middleware, and the store hook.
|
|
1026
|
-
├── index.ts # Main library entry point, re-exporting `createStore` and core types.
|
|
1027
|
-
├── package.json
|
|
1028
|
-
└── tsconfig.json
|
|
1029
|
-
```
|
|
1126
|
+
* `src/execution.ts`: Defines `ActionExecution` and `ActionTracker` classes for monitoring and maintaining a history of action dispatches, including their status and performance metrics.
|
|
1127
|
+
* `src/store.ts`: Contains the main `createStore` factory function. This module orchestrates the initialization of the core `ReactiveDataStore`, `StoreObserver`, `ActionTracker`, and `ArtifactContainer`. It also binds actions, applies middleware (`transform`, `validate`), and sets up the React hook using `useSyncExternalStore` for efficient component updates.
|
|
1128
|
+
* `src/types.ts`: Defines all core TypeScript interfaces and types for the store's public API, including `ActionContext`, `StoreDefinition`, `StoreHook`, middleware signatures (`Transformer`, `Validator`), and artifact-related types.
|
|
1030
1129
|
|
|
1031
1130
|
### Key External Dependencies
|
|
1032
1131
|
|
|
1033
1132
|
This library leverages the following `@asaidimu` packages for its core functionalities:
|
|
1034
1133
|
|
|
1035
|
-
* **`@asaidimu/utils-store`**: Provides the foundational `ReactiveDataStore` for immutable state management, transactions, core event emission, and the `
|
|
1134
|
+
* **`@asaidimu/utils-store`**: Provides the foundational `ReactiveDataStore` for immutable state management, transactions, core event emission, and the `StoreObserver` instance for deep insights into state changes.
|
|
1036
1135
|
* **`@asaidimu/utils-persistence`**: Offers various persistence adapters like `WebStoragePersistence` and `IndexedDBPersistence` for saving and loading state, including cross-tab synchronization.
|
|
1136
|
+
* **`@asaidimu/utils-artifacts`**: Provides the `ArtifactContainer` and related types for defining and resolving asynchronous, reactive dependencies (artifacts) within the store.
|
|
1037
1137
|
|
|
1038
1138
|
### Core Components
|
|
1039
1139
|
|
|
1040
1140
|
* **`ReactiveDataStore` (from `@asaidimu/utils-store`)**: The heart of the state management. It handles immutable state updates, middleware processing, transaction management, and emits detailed internal events about state changes.
|
|
1041
|
-
* **`
|
|
1141
|
+
* **`StoreObserver` (from `@asaidimu/utils-store`)**: Built on top of `ReactiveDataStore`'s event system, this component provides comprehensive debugging and monitoring features. This includes event history, state snapshots for time-travel, performance metrics, and utilities for logging or validation middleware.
|
|
1042
1142
|
* **`ActionTracker` (`src/execution.ts`)**: A dedicated class for tracking the lifecycle and performance of individual action executions, capturing details like start/end times, duration, parameters, and outcomes (success/error).
|
|
1043
|
-
* **`
|
|
1044
|
-
*
|
|
1143
|
+
* **`ArtifactContainer` (from `@asaidimu/utils-artifacts`)**: Manages the registration and resolution of artifacts. It handles dependencies between artifacts and reacts to state changes for artifacts that depend on store state.
|
|
1144
|
+
* **`createStore` Hook (`src/store.ts`)**: The primary React-facing API. It instantiates `ReactiveDataStore`, `StoreObserver`, `ActionTracker`, and `ArtifactContainer`. It wraps user-defined actions with debouncing and tracking, and provides the `select` function (powered by `useSyncExternalStore` for efficient component updates), the `watch` function for action loading states, and the `resolve` function for artifacts.
|
|
1145
|
+
* **Persistence Adapters (from `@asaidimu/utils-persistence`)**: Implement the `SimplePersistence` interface. `WebStoragePersistence` (for `localStorage`/`sessionStorage`) and `IndexedDBPersistence` provide concrete, ready-to-use storage solutions with cross-tab synchronization capabilities.
|
|
1045
1146
|
|
|
1046
1147
|
### Data Flow
|
|
1047
1148
|
|
|
1048
1149
|
1. **Action Dispatch**: A React component calls a bound action (e.g., `actions.addItem()`).
|
|
1049
1150
|
2. **Action Debouncing**: Actions are debounced by default (configurable), preventing rapid successive calls.
|
|
1050
|
-
3. **Action Loading State Update**: The store immediately updates the loading state for the dispatched action to `true` via `ReactiveDataStore`.
|
|
1151
|
+
3. **Action Loading State Update**: The store immediately updates the loading state for the dispatched action to `true` via an internal `ReactiveDataStore`.
|
|
1051
1152
|
4. **Action Execution Tracking**: The `ActionTracker` records the action's details (name, parameters, start time).
|
|
1052
|
-
5. **State Update Request**: The action's implementation returns a partial state update or a promise resolving to one.
|
|
1153
|
+
5. **State Update Request**: The action's implementation (receiving `ActionContext` with `state` and `resolve`) returns a partial state update or a promise resolving to one.
|
|
1053
1154
|
6. **Transaction Context**: If the action is wrapped within `store.transaction()`, the current state is snapshotted to enable potential rollback.
|
|
1054
|
-
7. **
|
|
1055
|
-
8. **
|
|
1155
|
+
7. **Validator Middleware**: The update first passes through any registered `validate` middleware. If any validator returns `false` or throws an error, the update is halted, and the state remains unchanged (and rolled back if in a transaction).
|
|
1156
|
+
8. **Transformer Middleware**: If not blocked, the update then passes through `transform` middleware. These functions can modify the partial update payload.
|
|
1056
1157
|
9. **State Merging**: The final, possibly transformed, update is immutably merged into the current state using `ReactiveDataStore`'s internal utility.
|
|
1057
1158
|
10. **Change Detection**: `ReactiveDataStore` performs a deep diff to identify precisely which paths in the state have changed.
|
|
1058
|
-
11. **Persistence**: If changes occurred, the new state is saved via the configured `
|
|
1059
|
-
12. **
|
|
1060
|
-
13. **
|
|
1061
|
-
14. **
|
|
1159
|
+
11. **Persistence**: If changes occurred, the new state is saved via the configured `SimplePersistence` adapter (e.g., `localStorage`, `IndexedDB`). The system also subscribes to external changes from persistence for cross-tab synchronization.
|
|
1160
|
+
12. **Artifact Re-evaluation**: If state changes affect an artifact that depends on that part of the state, `ArtifactContainer` may re-evaluate and re-resolve that artifact.
|
|
1161
|
+
13. **Listener Notification**: `React.useSyncExternalStore` subscribers (used by `select` and `resolve`) whose selected paths or resolved artifacts have changed are notified, triggering efficient re-renders of only the relevant components.
|
|
1162
|
+
14. **Action Loading State Reset**: Once the action completes (either successfully or with an error), the loading state for that action is reset to `false`.
|
|
1163
|
+
15. **Observability Events**: Throughout this entire flow, `ReactiveDataStore` emits fine-grained events (`update:start`, `middleware:complete`, `transaction:error`, etc.) which `StoreObserver` captures for debugging, metrics collection, and remote reporting.
|
|
1062
1164
|
|
|
1063
1165
|
### Extension Points
|
|
1064
1166
|
|
|
1065
|
-
* **Custom Middleware**: Easily add your own `
|
|
1066
|
-
* **Custom Persistence Adapters**: Implement the `
|
|
1167
|
+
* **Custom Middleware**: Easily add your own `Transformer` or `Validator` functions for custom logic (e.g., advanced logging, analytics, data transformation, or complex validation logic).
|
|
1168
|
+
* **Custom Persistence Adapters**: Implement the `SimplePersistence<T>` interface (from `@asaidimu/utils-persistence`) to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store).
|
|
1169
|
+
* **Custom Artifact Factories**: Define factories for any external service, resource, or complex derived state, allowing for clear separation of concerns and reactive dependency injection.
|
|
1067
1170
|
* **Remote Observability Destinations**: Create new `RemoteDestination` implementations (part of `@asaidimu/utils-store`) to send metrics and traces to any external observability platform not already supported by default.
|
|
1068
1171
|
|
|
1069
1172
|
## Development & Contributing
|
|
@@ -1089,7 +1192,7 @@ We welcome contributions! Please follow the guidelines below.
|
|
|
1089
1192
|
* `bun test`: Runs all unit tests using `Vitest` in interactive watch mode.
|
|
1090
1193
|
* `bun test:ci`: Runs all unit tests once, suitable for CI/CD pipelines.
|
|
1091
1194
|
* `bun clean`: Removes the `dist` directory, cleaning up previous build artifacts.
|
|
1092
|
-
* `bun prebuild`: Pre-build step that cleans the `dist` directory and runs an internal package synchronization script.
|
|
1195
|
+
* `bun prebuild`: Pre-build step that cleans the `dist` directory and runs an internal package synchronization script (`.sync-package.ts`).
|
|
1093
1196
|
* `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies the code using `Terser`.
|
|
1094
1197
|
* `bun dev`: Starts the e-commerce dashboard demonstration application using `Vite`.
|
|
1095
1198
|
* `bun postbuild`: Post-build step that copies `README.md`, `LICENSE.md`, and the specialized `dist.package.json` into the `dist` folder, preparing the package for publishing.
|
|
@@ -1127,9 +1230,10 @@ For bugs, feature requests, or questions, please open an issue on the [GitHub Is
|
|
|
1127
1230
|
3. **Persistence**:
|
|
1128
1231
|
* Use unique `storeId` or `storageKey` for each distinct store to avoid data conflicts.
|
|
1129
1232
|
* Always check the `isReady` flag for UI elements that depend on the initial state loaded from persistence, to prevent rendering incomplete data.
|
|
1130
|
-
4. **Middleware**: Leverage
|
|
1233
|
+
4. **Middleware**: Leverage `transform` and `validate` for cross-cutting concerns like logging, analytics, data transformation, or complex validation logic that applies to multiple actions. They now receive `ActionContext`, allowing for advanced logic including artifact resolution.
|
|
1131
1234
|
5. **`Symbol.for("delete")`**: Use this explicit symbol for property removal to maintain clarity and avoid accidental data mutations or unexpected behavior when merging partial updates.
|
|
1132
1235
|
6. **Debounce Time**: Adjust the `debounceTime` in `createStore` options for actions that might be called rapidly (e.g., search input, scroll events) to optimize performance. A `debounceTime` of `0` means actions execute immediately.
|
|
1236
|
+
7. **Artifacts**: Use artifacts to manage external dependencies, services, or complex derived values that might change over time or have their own lifecycle. This promotes better separation of concerns and testability.
|
|
1133
1237
|
|
|
1134
1238
|
### API Reference
|
|
1135
1239
|
|
|
@@ -1141,18 +1245,18 @@ The main entry point for creating a store hook.
|
|
|
1141
1245
|
// From src/types.ts
|
|
1142
1246
|
export interface StoreDefinition<
|
|
1143
1247
|
TState extends object,
|
|
1144
|
-
TArtifactsMap extends ArtifactsMap<TState>,
|
|
1248
|
+
TArtifactsMap extends ArtifactsMap<TState>,
|
|
1145
1249
|
TActions extends ActionMap<TState, TArtifactsMap>,
|
|
1146
1250
|
> {
|
|
1147
1251
|
state: TState;
|
|
1148
1252
|
actions: TActions;
|
|
1149
1253
|
artifacts?: TArtifactsMap; // Optional artifact definitions
|
|
1150
|
-
|
|
1151
|
-
|
|
1254
|
+
transform?: Record<string, Transformer<TState, TArtifactsMap>>; // Optional transforming middleware
|
|
1255
|
+
validate?: Record<string, Validator<TState, TArtifactsMap>>; // Optional blocking middleware
|
|
1152
1256
|
}
|
|
1153
1257
|
|
|
1154
1258
|
interface StoreOptions<T> extends ObserverOptions { // ObserverOptions from @asaidimu/utils-store
|
|
1155
|
-
enableMetrics?: boolean; // Enable
|
|
1259
|
+
enableMetrics?: boolean; // Enable StoreObserver and ActionTracker features (default: false)
|
|
1156
1260
|
persistence?: SimplePersistence<T>; // Optional persistence adapter instance (from @asaidimu/utils-persistence)
|
|
1157
1261
|
debounceTime?: number; // Time in milliseconds to debounce actions (default: 0ms)
|
|
1158
1262
|
// Other ObserverOptions for logging, performanceThresholds, maxEvents, maxStateHistory are inherited
|
|
@@ -1164,28 +1268,27 @@ const useStoreHook = createStore(definition, options);
|
|
|
1164
1268
|
**Returns**: A React hook (`useStoreHook`) which, when called in a component, returns an object with the following properties:
|
|
1165
1269
|
|
|
1166
1270
|
* `store`: Direct access to the underlying `ReactiveDataStore` instance (from `@asaidimu/utils-store`). This provides low-level control and event subscription.
|
|
1167
|
-
* `observer`: The `
|
|
1271
|
+
* `observer`: The `StoreObserver` instance (from `@asaidimu/utils-store`). Available only if `enableMetrics` is `true`. Provides debug, time-travel, and monitoring utilities.
|
|
1168
1272
|
* `select`: A memoized selector function `(<S>(selector: (state: TState) => S) => S)`. Extracts specific state slices. Re-renders components only when selected data changes.
|
|
1169
1273
|
* `actions`: An object containing your defined actions, fully typed and bound to the store. These actions are debounced (if `debounceTime` > 0) and their loading states are tracked. Each action returns a `Promise<TState>` resolving to the new state after the action completes.
|
|
1170
1274
|
* `actionTracker`: An instance of `ActionTracker` (from `src/execution.ts`). Available only if `enableMetrics` is `true`. Provides methods for monitoring the execution history of your actions.
|
|
1171
1275
|
* `state`: A getter function `(() => TState)` that returns the entire current state object. Use sparingly, as components relying on this will re-render on *any* state change. `select` is generally preferred for performance.
|
|
1172
1276
|
* `isReady`: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
|
|
1173
1277
|
* `watch`: A function `(<K extends keyof TActions>(action: K) => boolean)` to watch the loading status of individual actions. Returns `true` if the action is currently executing.
|
|
1174
|
-
* `resolve`: A reactive artifact resolver `(<K extends keyof TArtifactsMap>(key: K) =>
|
|
1278
|
+
* `resolve`: A reactive artifact resolver `(<K extends keyof TArtifactsMap>(key: K) => ResolvedArtifact<ArtifactValue<TArtifactsMap[K]>>)`. If `artifacts` are defined in the store, this hook returns `ResolvedArtifact` (containing `instance` and `ready` status) for a specific artifact, reactively updating if the artifact instance changes or becomes ready.
|
|
1175
1279
|
|
|
1176
1280
|
#### `ReactiveDataStore` (accessed via `useStoreHook().store` from `@asaidimu/utils-store`)
|
|
1177
1281
|
|
|
1178
1282
|
* `get(clone?: boolean): TState`: Retrieves the current state. Pass `true` to get a deep clone (recommended for mutations outside of actions).
|
|
1179
1283
|
* `set(update: StateUpdater<TState>): Promise<void>`: Updates the state with a partial object or a function returning a partial object.
|
|
1180
|
-
* `
|
|
1284
|
+
* `watch(path: string | string[], callback: (state: TState) => void): () => void`: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.
|
|
1181
1285
|
* `transaction<R>(operation: () => R | Promise<R>): Promise<R>`: Executes a function as an atomic transaction. Rolls back all changes if an error occurs if the operation throws.
|
|
1182
|
-
* `use(middleware:
|
|
1183
|
-
* `useBlockingMiddleware(middleware: BlockingMiddleware<TState>, name?: string): string`: Adds a blocking middleware. Returns its ID.
|
|
1286
|
+
* `use(middleware: StoreMiddleware<TState>): string`: Adds a transforming middleware. Returns its ID. (Note: The `createStore` API uses `transform` and `validate` which internally map to `ReactiveDataStore.use`).
|
|
1184
1287
|
* `removeMiddleware(id: string): boolean`: Removes a middleware by its ID.
|
|
1185
1288
|
* `isReady(): boolean`: Checks if the persistence layer has loaded its initial state.
|
|
1186
1289
|
* `onStoreEvent(event: StoreEvent, listener: (data: any) => void): () => void`: Subscribes to internal store events (e.g., `'update:complete'`, `'middleware:error'`, `'transaction:start'`).
|
|
1187
1290
|
|
|
1188
|
-
#### `
|
|
1291
|
+
#### `StoreObserver` (accessed via `useStoreHook().observer` from `@asaidimu/utils-store`)
|
|
1189
1292
|
|
|
1190
1293
|
Available only if `enableMetrics` is `true` in `createStore` options.
|
|
1191
1294
|
|
|
@@ -1194,8 +1297,6 @@ Available only if `enableMetrics` is `true` in `createStore` options.
|
|
|
1194
1297
|
* `getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: DeepPartial<TState>; to: DeepPartial<TState>; }>`: Provides a simplified view of recent state changes.
|
|
1195
1298
|
* `getPerformanceMetrics(): StoreMetrics`: Returns an object containing performance statistics (e.g., `updateCount`, `averageUpdateTime`).
|
|
1196
1299
|
* `createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }`: Returns controls for time-travel debugging.
|
|
1197
|
-
* `createLoggingMiddleware(options?: object): Middleware<TState>`: A factory for a simple logging middleware.
|
|
1198
|
-
* `createValidationMiddleware(validator: (state: TState, update: DeepPartial<TState>) => boolean | { valid: boolean; reason?: string }): BlockingMiddleware<TState>`: A factory for a schema validation middleware.
|
|
1199
1300
|
* `clearHistory(): void`: Clears the event and state history.
|
|
1200
1301
|
* `disconnect(): void`: Cleans up all listeners and resources.
|
|
1201
1302
|
|
|
@@ -1224,10 +1325,10 @@ All adapters implement `SimplePersistence<T>`:
|
|
|
1224
1325
|
| **Feature** | **@asaidimu/react-store** | **Redux** | **Zustand** | **MobX** | **Recoil** |
|
|
1225
1326
|
| :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
|
|
1226
1327
|
| **Dev Experience** | Intuitive hook-based API with rich tooling. | Verbose setup with reducers and middleware. | Minimalist, hook-friendly API. | Reactive, class-based approach. | Atom-based, React-native feel. |
|
|
1227
|
-
| **Learning Curve** | Moderate (middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). |
|
|
1328
|
+
| **Learning Curve** | Moderate (artifacts, middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). |
|
|
1228
1329
|
| **API Complexity** | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
|
|
1229
|
-
| **Scalability** | High (transactions, persistence, remote metrics). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
|
|
1230
|
-
| **Extensibility** | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
|
|
1330
|
+
| **Scalability** | High (transactions, persistence, remote metrics, artifacts). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
|
|
1331
|
+
| **Extensibility** | Excellent (middleware, custom persistence, observability, artifacts). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
|
|
1231
1332
|
| **Performance** | Optimized (selectors, reactive updates via `useSyncExternalStore`). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
|
|
1232
1333
|
| **Bundle Size** | Moderate (includes observability, persistence, remote observability framework). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
|
|
1233
1334
|
| **Persistence** | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
|
|
@@ -1235,13 +1336,13 @@ All adapters implement `SimplePersistence<T>`:
|
|
|
1235
1336
|
| **React Integration** | Native (hooks, `useSyncExternalStore`). | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
|
|
1236
1337
|
|
|
1237
1338
|
#### Where `@asaidimu/react-store` Shines
|
|
1238
|
-
* **All-in-One**: It aims to be a single solution for state management, persistence, and
|
|
1339
|
+
* **All-in-One**: It aims to be a single solution for state management, persistence, observability, and artifact management, reducing the need for multiple external dependencies and their integration complexities.
|
|
1239
1340
|
* **Flexibility**: The robust middleware system, transaction support, and artifact management make it highly adaptable to complex business logic, asynchronous data flows, and dependency injection patterns.
|
|
1240
1341
|
* **Modern React**: It leverages `useSyncExternalStore` for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders with minimal overhead.
|
|
1241
1342
|
|
|
1242
1343
|
#### Trade-Offs
|
|
1243
1344
|
* **Bundle Size**: While comprehensive, it naturally has a larger bundle size compared to minimalist alternatives like Zustand, as it includes a wider range of features out-of-the-box. Tree-shaking is applied, but the rich feature set contributes to the baseline.
|
|
1244
|
-
* **Learning Curve**: The rich feature set and advanced concepts (middleware, transactions, observability) might present a slightly steeper initial learning curve for developers new to advanced state management, though the API strives for simplicity and clear documentation.
|
|
1345
|
+
* **Learning Curve**: The rich feature set and advanced concepts (middleware, transactions, observability, artifacts) might present a slightly steeper initial learning curve for developers new to advanced state management, though the API strives for simplicity and clear documentation.
|
|
1245
1346
|
|
|
1246
1347
|
### Troubleshooting
|
|
1247
1348
|
|
|
@@ -1257,12 +1358,18 @@ All adapters implement `SimplePersistence<T>`:
|
|
|
1257
1358
|
* Check the `debounceTime` option in your `createStore` configuration. If set, actions will be delayed.
|
|
1258
1359
|
* If actions are `async`, ensure you `await` them where necessary in your components.
|
|
1259
1360
|
* Use the `watch` function to check if an action is in a loading state.
|
|
1260
|
-
* **Middleware not working:**
|
|
1261
|
-
* Ensure
|
|
1361
|
+
* **Middleware (transform/validate) not working:**
|
|
1362
|
+
* Ensure `transform` and `validate` functions are correctly registered in the `createStore` definition.
|
|
1262
1363
|
* Check console logs (if `enableConsoleLogging` is `true`) for `middleware:start`, `middleware:complete`, or `middleware:error` events.
|
|
1364
|
+
* Verify the middleware signature matches the `Transformer` or `Validator` types, especially the `ActionContext` parameter.
|
|
1365
|
+
* **Artifacts not resolving or updating:**
|
|
1366
|
+
* Check the `ready` property returned by the `resolve` hook. Artifacts are often asynchronous.
|
|
1367
|
+
* Ensure that any dependencies an artifact has (e.g., other artifacts, state values) are stable or correctly configured for reactivity.
|
|
1368
|
+
* Verify the `factory` function for your artifact is correctly defined and returns the expected value.
|
|
1263
1369
|
* **TypeScript errors:**
|
|
1264
1370
|
* Verify your state interfaces match the `initialState` structure.
|
|
1265
1371
|
* Ensure action return types are `DeepPartial<TState>` as expected.
|
|
1372
|
+
* Make sure `transform` and `validate` functions adhere to their new `ActionContext` signature.
|
|
1266
1373
|
* Update `@asaidimu/react-store` and `@types/react` to their latest compatible versions.
|
|
1267
1374
|
|
|
1268
1375
|
### FAQ
|
|
@@ -1271,16 +1378,16 @@ All adapters implement `SimplePersistence<T>`:
|
|
|
1271
1378
|
A: `useSyncExternalStore` is generally a client-side hook. While the underlying `ReactiveDataStore` is framework-agnostic, the `createStore` hook is designed for client-side React components and is not directly compatible with RSCs for reactive state consumption. You can, however, hydrate the client-side store with initial state from RSCs.
|
|
1272
1379
|
|
|
1273
1380
|
**Q: How do I share state between multiple stores?**
|
|
1274
|
-
A: Currently, `@asaidimu/react-store` promotes independent stores. For shared logic or derived state, consider creating a utility function that both stores can call, or a parent component that manages shared data and passes it down. Direct cross-store subscription or merging is not a primary pattern.
|
|
1381
|
+
A: Currently, `@asaidimu/react-store` promotes independent stores. For shared logic or derived state, consider creating an artifact that both stores can resolve, or a utility function that both stores can call, or a parent component that manages shared data and passes it down. Direct cross-store subscription or merging is not a primary pattern.
|
|
1275
1382
|
|
|
1276
1383
|
**Q: Can I use it without React?**
|
|
1277
|
-
A: The core state management (`ReactiveDataStore`, `
|
|
1384
|
+
A: The core state management (`ReactiveDataStore`, `StoreObserver` from `@asaidimu/utils-store`) is framework-agnostic. The `createStore` function is React-specific, but you can use the underlying `ReactiveDataStore` directly in non-React environments.
|
|
1278
1385
|
|
|
1279
1386
|
**Q: What about immutability?**
|
|
1280
1387
|
A: All state updates are handled immutably. The library ensures that direct state mutations within actions are prevented and that new state objects are created for changed paths, preserving reference equality for unchanged parts of the state.
|
|
1281
1388
|
|
|
1282
1389
|
**Q: Is there a dev tools extension?**
|
|
1283
|
-
A: Currently, there is no dedicated browser extension. However, the built-in `
|
|
1390
|
+
A: Currently, there is no dedicated browser extension. However, the built-in `StoreObserver` (`observer` object) and `ActionTracker` provide extensive programmatic debugging capabilities, including event history, state snapshots, and performance metrics, which can be integrated into your application's dev panel. Console logging can also be enabled for quick insights.
|
|
1284
1391
|
|
|
1285
1392
|
### Changelog
|
|
1286
1393
|
|
|
@@ -1293,4 +1400,4 @@ This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.m
|
|
|
1293
1400
|
### Acknowledgments
|
|
1294
1401
|
|
|
1295
1402
|
Developed by [Saidimu](https://github.com/asaidimu).
|
|
1296
|
-
This library leverages the robust capabilities of [@asaidimu/utils-store](https://github.com/asaidimu/utils-store)
|
|
1403
|
+
This library leverages the robust capabilities of [@asaidimu/utils-store](https://github.com/asaidimu/utils-store), [@asaidimu/utils-persistence](https://github.com/asaidimu/utils-persistence), and [@asaidimu/utils-artifacts](https://github.com/asaidimu/utils-artifacts) for its core state management, persistence, and artifact management features.
|