@asaidimu/react-store 2.0.0 → 3.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 +516 -263
- package/index.cjs +1 -1
- package/index.d.cts +57 -60
- package/index.d.ts +57 -60
- package/index.js +1 -1
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@asaidimu/react-store)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/asaidimu/node-react/actions)
|
|
5
6
|
|
|
6
7
|
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware system.
|
|
7
8
|
|
|
@@ -24,6 +25,7 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
24
25
|
* [Remote Observability](#remote-observability)
|
|
25
26
|
* [Transaction Support](#transaction-support)
|
|
26
27
|
* [Event System](#event-system)
|
|
28
|
+
* [Watching Action Loading States](#watching-action-loading-states)
|
|
27
29
|
* [Advanced Hook Properties](#advanced-hook-properties)
|
|
28
30
|
* [Project Architecture](#project-architecture)
|
|
29
31
|
* [Development & Contributing](#development--contributing)
|
|
@@ -31,6 +33,8 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
31
33
|
* [Best Practices](#best-practices)
|
|
32
34
|
* [API Reference](#api-reference)
|
|
33
35
|
* [Comparison with Other State Management Solutions](#comparison-with-other-state-management-solutions)
|
|
36
|
+
* [Troubleshooting](#troubleshooting)
|
|
37
|
+
* [FAQ](#faq)
|
|
34
38
|
* [Changelog](#changelog)
|
|
35
39
|
* [License](#license)
|
|
36
40
|
* [Acknowledgments](#acknowledgments)
|
|
@@ -41,19 +45,19 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
41
45
|
|
|
42
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.
|
|
43
47
|
|
|
44
|
-
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.
|
|
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 actions.
|
|
45
49
|
|
|
46
50
|
### Key Features
|
|
47
51
|
|
|
48
|
-
* 📊 **Reactive State Management**: Automatically tracks dependencies to optimize component renders and ensure efficient updates
|
|
49
|
-
* 🛡️ **Type-Safe**:
|
|
52
|
+
* 📊 **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.
|
|
50
54
|
* ⚙️ **Middleware Pipeline**: Implement custom logic to transform, validate, or log state changes before they are applied. Supports both transforming and blocking middleware.
|
|
51
55
|
* 📦 **Transaction Support**: Group multiple state updates into a single atomic operation, with automatic rollback if any part of the transaction fails.
|
|
52
56
|
* 💾 **Built-in Persistence**: Seamlessly integrate with web storage mechanisms like `IndexedDB` and `WebStorage` (localStorage/sessionStorage), including cross-tab synchronization.
|
|
53
|
-
* 🔍 **Deep Observability**: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* ⚛️ **React
|
|
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 `StoreObservability` instance.
|
|
58
|
+
* ⚡ **Performance Optimized**: Features intelligent selector caching (now using `WeakMap` for enhanced efficiency) and debounced actions with configurable immediate execution to prevent rapid successive calls and ensure smooth application performance.
|
|
59
|
+
* 🚀 **Action Loading States**: Track the real-time loading status of individual actions, providing immediate feedback on asynchronous operations.
|
|
60
|
+
* ⚛️ **React 19+ Ready**: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
|
|
57
61
|
* 🗑️ **Explicit Deletions**: Use `Symbol.for("delete")` to explicitly remove properties from nested state objects.
|
|
58
62
|
|
|
59
63
|
## Installation & Setup
|
|
@@ -61,8 +65,8 @@ Designed with modern React in mind, it leverages `useSyncExternalStore` for opti
|
|
|
61
65
|
### Prerequisites
|
|
62
66
|
|
|
63
67
|
* Node.js (v18 or higher recommended)
|
|
64
|
-
* React (
|
|
65
|
-
* A package manager like `bun`, `npm`, or `yarn`.
|
|
68
|
+
* React (v19 or higher recommended)
|
|
69
|
+
* A package manager like `bun`, `npm`, or `yarn`. This project explicitly uses `bun`.
|
|
66
70
|
|
|
67
71
|
### Installation Steps
|
|
68
72
|
|
|
@@ -78,7 +82,7 @@ yarn add @asaidimu/react-store
|
|
|
78
82
|
|
|
79
83
|
### Configuration
|
|
80
84
|
|
|
81
|
-
No global configuration is required. All options are passed during store creation.
|
|
85
|
+
No global configuration is required. All options are passed during store creation via the `createStore` function. Configuration includes enabling metrics, logging, persistence, and performance thresholds.
|
|
82
86
|
|
|
83
87
|
### Verification
|
|
84
88
|
|
|
@@ -87,99 +91,242 @@ You can verify the installation by importing `createStore` and setting up a basi
|
|
|
87
91
|
```typescript
|
|
88
92
|
import { createStore } from '@asaidimu/react-store';
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
interface MyState {
|
|
95
|
+
value: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const myStore = createStore<MyState, any>({
|
|
91
99
|
state: { value: 'hello' },
|
|
92
100
|
actions: {
|
|
93
|
-
setValue: (
|
|
101
|
+
setValue: (_, newValue: string) => ({ value: newValue }),
|
|
94
102
|
},
|
|
95
103
|
});
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
const { select, actions } = myStore(); // Instantiate the hook
|
|
106
|
+
|
|
107
|
+
// Access state
|
|
108
|
+
const currentValue = select(s => s.value);
|
|
109
|
+
console.log(currentValue); // Expected: 'hello'
|
|
110
|
+
|
|
111
|
+
// Dispatch an action
|
|
112
|
+
actions.setValue('world');
|
|
98
113
|
```
|
|
99
114
|
|
|
100
|
-
If no errors are thrown, the package is correctly installed.
|
|
115
|
+
If no errors are thrown during installation or when running this basic example, the package is correctly installed and configured.
|
|
101
116
|
|
|
102
117
|
## Usage Documentation
|
|
103
118
|
|
|
104
119
|
### Creating a Store
|
|
105
120
|
|
|
106
|
-
Define your application state and actions, then create a store using `createStore`.
|
|
121
|
+
Define your application state and actions, then create a store using `createStore`. The `actions` object maps action names to functions that receive an `ActionContext` (containing the current `state` and an `resolve` function for artifacts) and any additional arguments.
|
|
107
122
|
|
|
108
123
|
```tsx
|
|
109
|
-
//
|
|
110
|
-
import { createStore } from '@asaidimu/react-store';
|
|
124
|
+
// ui/store.tsx (Example from codebase)
|
|
125
|
+
import { createStore } from '@asaidimu/react-store'; // Assuming direct import or wrapper
|
|
126
|
+
|
|
127
|
+
export interface Product {
|
|
128
|
+
id: number;
|
|
129
|
+
name: string;
|
|
130
|
+
price: number;
|
|
131
|
+
stock: number;
|
|
132
|
+
image: string;
|
|
133
|
+
}
|
|
111
134
|
|
|
112
|
-
interface
|
|
113
|
-
|
|
114
|
-
user: { name: string; loggedIn: boolean; email?: string };
|
|
115
|
-
settings: { theme: 'light' | 'dark'; notifications: boolean };
|
|
135
|
+
export interface CartItem extends Product {
|
|
136
|
+
quantity: number;
|
|
116
137
|
}
|
|
117
138
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
export interface Order {
|
|
140
|
+
id: string;
|
|
141
|
+
items: CartItem[];
|
|
142
|
+
total: number;
|
|
143
|
+
date: Date;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ECommerceState extends Record<string, any>{
|
|
147
|
+
products: Product[];
|
|
148
|
+
cart: CartItem[];
|
|
149
|
+
orders: Order[];
|
|
150
|
+
topSellers: { id: number; name: string; sales: number }[];
|
|
151
|
+
activeUsers: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const initialState: ECommerceState = {
|
|
155
|
+
products: [
|
|
156
|
+
{ id: 1, name: 'Wireless Mouse', price: 25.99, stock: 150, image: 'https://placehold.co/600x400/white/black?text=Mouse' },
|
|
157
|
+
{ id: 2, name: 'Mechanical Keyboard', price: 79.99, stock: 100, image: 'https://placehold.co/600x400/white/black?text=Keyboard' },
|
|
158
|
+
{ id: 3, name: '4K Monitor', price: 349.99, stock: 75, image: 'https://placehold.co/600x400/white/black?text=Monitor' },
|
|
159
|
+
{ id: 4, name: 'Webcam', price: 45.50, stock: 120, image: 'https://placehold.co/600x400/white/black?text=Webcam' },
|
|
160
|
+
{ id: 5, name: 'USB-C Hub', price: 39.99, stock: 200, image: 'https://placehold.co/600x400/white/black?text=Hub' },
|
|
161
|
+
],
|
|
162
|
+
cart: [],
|
|
163
|
+
orders: [],
|
|
164
|
+
topSellers: [
|
|
165
|
+
{ id: 2, name: 'Mechanical Keyboard', sales: 120 },
|
|
166
|
+
{ id: 3, name: '4K Monitor', sales: 85 },
|
|
167
|
+
{ id: 1, name: 'Wireless Mouse', sales: 80 },
|
|
168
|
+
{ id: 5, name: 'USB-C Hub', sales: 70 },
|
|
169
|
+
{ id: 4, name: 'Webcam', sales: 65 },
|
|
170
|
+
],
|
|
171
|
+
activeUsers: 1428,
|
|
122
172
|
};
|
|
123
173
|
|
|
124
|
-
const
|
|
125
|
-
state:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
const actions = {
|
|
175
|
+
addToCart: ({ state }: any, product: Product) => {
|
|
176
|
+
const existingItem = state.cart.find((item:any) => item.id === product.id);
|
|
177
|
+
if (existingItem) {
|
|
178
|
+
return {
|
|
179
|
+
cart: state.cart.map((item:any) =>
|
|
180
|
+
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
|
|
181
|
+
),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { cart: [...state.cart, { ...product, quantity: 1 }] };
|
|
185
|
+
},
|
|
186
|
+
removeFromCart: ({state}: {state:ECommerceState}, productId: number) => ({
|
|
187
|
+
cart: state.cart.filter((item) => item.id !== productId),
|
|
188
|
+
}),
|
|
189
|
+
updateQuantity: ({state}: {state:ECommerceState}, { productId, quantity }: { productId: number; quantity: number }) => ({
|
|
190
|
+
cart: state.cart.map((item) =>
|
|
191
|
+
item.id === productId ? { ...item, quantity } : item
|
|
192
|
+
).filter(item => item.quantity > 0),
|
|
193
|
+
}),
|
|
194
|
+
checkout: ({state}: {state:ECommerceState}) => {
|
|
195
|
+
const total = state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
196
|
+
const newOrder: Order = {
|
|
197
|
+
id: crypto.randomUUID(),
|
|
198
|
+
items: state.cart,
|
|
199
|
+
total,
|
|
200
|
+
date: new Date(),
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
cart: [],
|
|
204
|
+
orders: [newOrder, ...state.orders],
|
|
205
|
+
products: state.products.map(p => {
|
|
206
|
+
const cartItem = state.cart.find(item => item.id === p.id);
|
|
207
|
+
return cartItem ? { ...p, stock: p.stock - cartItem.quantity } : p;
|
|
208
|
+
}),
|
|
209
|
+
topSellers: state.topSellers.map(s => {
|
|
210
|
+
const cartItem = state.cart.find(item => item.id === s.id);
|
|
211
|
+
return cartItem ? { ...s, sales: s.sales + cartItem.quantity } : s;
|
|
212
|
+
}).sort((a, b) => b.sales - a.sales),
|
|
213
|
+
};
|
|
136
214
|
},
|
|
137
|
-
}, {
|
|
138
|
-
debounceTime: 100, // Debounce actions by 100ms
|
|
139
|
-
enableConsoleLogging: true, // Enable console logs for store events
|
|
140
|
-
logEvents: { updates: true, transactions: true, middleware: true },
|
|
141
|
-
performanceThresholds: { updateTime: 20, middlewareTime: 5 }, // Warn for slow operations
|
|
142
|
-
});
|
|
143
215
|
|
|
144
|
-
|
|
145
|
-
|
|
216
|
+
updateStock: ({state}: {state:ECommerceState}) => ({
|
|
217
|
+
products: state.products.map((p:any) => ({
|
|
218
|
+
...p,
|
|
219
|
+
stock: Math.max(0, p.stock + Math.floor(Math.random() * 10) - 5)
|
|
220
|
+
}))
|
|
221
|
+
}),
|
|
222
|
+
updateActiveUsers: ({state}: {state:ECommerceState}) => ({
|
|
223
|
+
activeUsers: state.activeUsers + Math.floor(Math.random() * 20) - 10,
|
|
224
|
+
}),
|
|
225
|
+
addRandomOrder: ({state}: {state:ECommerceState}) => {
|
|
226
|
+
const randomProduct = state.products[Math.floor(Math.random() * state.products.length)];
|
|
227
|
+
const quantity = Math.floor(Math.random() * 3) + 1;
|
|
228
|
+
const newOrder: Order = {
|
|
229
|
+
id: crypto.randomUUID(),
|
|
230
|
+
items: [{ ...randomProduct, quantity }],
|
|
231
|
+
total: randomProduct.price * quantity,
|
|
232
|
+
date: new Date(),
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
orders: [newOrder, ...state.orders],
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const useStore = createStore(
|
|
241
|
+
{
|
|
242
|
+
state: initialState,
|
|
243
|
+
actions,
|
|
244
|
+
},
|
|
245
|
+
{ enableMetrics: true } // Enables metrics for observability
|
|
246
|
+
);
|
|
146
247
|
```
|
|
147
248
|
|
|
148
249
|
### Using in Components
|
|
149
250
|
|
|
150
|
-
Consume your store's state and actions within your React components using the exported hook.
|
|
251
|
+
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.
|
|
151
252
|
|
|
152
253
|
```tsx
|
|
153
|
-
//
|
|
154
|
-
import
|
|
155
|
-
import {
|
|
254
|
+
// ui/App.tsx (Excerpt from codebase)
|
|
255
|
+
import { useEffect, useMemo } from 'react';
|
|
256
|
+
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
257
|
+
import { useStore, Product, CartItem, Order } from './store'; // Assuming these imports resolve correctly
|
|
156
258
|
|
|
157
|
-
|
|
158
|
-
const { select, actions, isReady } = useMyStore();
|
|
259
|
+
// ... (ShadCN-like UI Components for brevity) ...
|
|
159
260
|
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const theme = select((state) => state.settings.theme);
|
|
261
|
+
const ProductCatalog = () => {
|
|
262
|
+
const { select, actions } = useStore();
|
|
263
|
+
const products = select((state) => state.products); // Granular selection
|
|
164
264
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
265
|
+
return (
|
|
266
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
267
|
+
{products.map((product: Product) => (
|
|
268
|
+
<Card key={product.id}>
|
|
269
|
+
<CardHeader>
|
|
270
|
+
<CardTitle>{product.name}</CardTitle>
|
|
271
|
+
</CardHeader>
|
|
272
|
+
<CardContent className="flex-grow">
|
|
273
|
+
<img src={product.image} alt={product.name} className="w-full h-40 object-cover rounded-lg mb-4" />
|
|
274
|
+
<div className="flex justify-between items-center">
|
|
275
|
+
<p className="text-lg font-semibold text-gray-700">${product.price.toFixed(2)}</p>
|
|
276
|
+
<p className="text-sm text-gray-500">{product.stock} in stock</p>
|
|
277
|
+
</div>
|
|
278
|
+
</CardContent>
|
|
279
|
+
<CardFooter>
|
|
280
|
+
<Button onClick={() => actions.addToCart(product)} className="w-full">Add to Cart</Button>
|
|
281
|
+
</CardFooter>
|
|
282
|
+
</Card>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
function App() {
|
|
289
|
+
const { actions } = useStore();
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
// Real-time simulations for the dashboard
|
|
293
|
+
const stockInterval = setInterval(() => actions.updateStock(), 2000);
|
|
294
|
+
const usersInterval = setInterval(() => actions.updateActiveUsers(), 3000);
|
|
295
|
+
const ordersInterval = setInterval(() => actions.addRandomOrder(), 5000);
|
|
296
|
+
|
|
297
|
+
return () => {
|
|
298
|
+
clearInterval(stockInterval);
|
|
299
|
+
clearInterval(usersInterval);
|
|
300
|
+
clearInterval(ordersInterval);
|
|
301
|
+
};
|
|
302
|
+
}, [actions]); // `actions` object is stable due to `useMemo` in createStore
|
|
169
303
|
|
|
170
304
|
return (
|
|
171
|
-
<div
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
305
|
+
<div className="bg-gray-50 text-gray-900 min-h-screen">
|
|
306
|
+
<header className="border-b">
|
|
307
|
+
<div className="container mx-auto px-4 h-16 flex items-center">
|
|
308
|
+
<h1 className="text-xl font-bold">E-Commerce Dashboard</h1>
|
|
309
|
+
</div>
|
|
310
|
+
</header>
|
|
311
|
+
<main className="container mx-auto p-4 sm:p-6 lg:p-8">
|
|
312
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
313
|
+
<div className="lg:col-span-2 space-y-6">
|
|
314
|
+
<ProductCatalog />
|
|
315
|
+
<LiveInventoryChart />
|
|
316
|
+
</div>
|
|
317
|
+
<div className="space-y-6">
|
|
318
|
+
<ShoppingCart />
|
|
319
|
+
<UserAnalytics />
|
|
320
|
+
<TopSellingProducts />
|
|
321
|
+
<RecentOrdersFeed />
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</main>
|
|
178
325
|
</div>
|
|
179
326
|
);
|
|
180
327
|
}
|
|
181
328
|
|
|
182
|
-
export default
|
|
329
|
+
export default App;
|
|
183
330
|
```
|
|
184
331
|
|
|
185
332
|
### Handling Deletions
|
|
@@ -203,12 +350,12 @@ const deleteStore = createStore({
|
|
|
203
350
|
tags: ['electronics', 'new']
|
|
204
351
|
},
|
|
205
352
|
actions: {
|
|
206
|
-
removeDetails: (
|
|
207
|
-
removeDimensions: (
|
|
208
|
-
removeTag: (state, tagToRemove: string) => ({
|
|
353
|
+
removeDetails: (ctx) => ({ details: Symbol.for("delete") }),
|
|
354
|
+
removeDimensions: (ctx) => ({ details: { dimensions: Symbol.for("delete") } }),
|
|
355
|
+
removeTag: ({state}, tagToRemove: string) => ({
|
|
209
356
|
tags: state.tags.filter(tag => tag !== tagToRemove)
|
|
210
357
|
}),
|
|
211
|
-
clearAllExceptId: (
|
|
358
|
+
clearAllExceptId: (ctx) => ({
|
|
212
359
|
name: Symbol.for("delete"),
|
|
213
360
|
details: Symbol.for("delete"),
|
|
214
361
|
tags: Symbol.for("delete")
|
|
@@ -244,10 +391,12 @@ runDeleteExample();
|
|
|
244
391
|
|
|
245
392
|
### Persistence
|
|
246
393
|
|
|
247
|
-
Persist your store's state across browser sessions or synchronize it across multiple tabs.
|
|
394
|
+
Persist your store's state across browser sessions or synchronize it across multiple tabs using persistence adapters from `@asaidimu/utils-persistence`. You can choose between `WebStoragePersistence` (for `localStorage` or `sessionStorage`) and `IndexedDBPersistence` for more robust storage.
|
|
248
395
|
|
|
249
396
|
```tsx
|
|
250
|
-
import { createStore
|
|
397
|
+
import { createStore } from '@asaidimu/react-store';
|
|
398
|
+
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
|
|
399
|
+
import React, { useEffect } from 'react';
|
|
251
400
|
|
|
252
401
|
// 1. Using WebStoragePersistence (localStorage by default)
|
|
253
402
|
// Data persists even if the browser tab is closed and reopened.
|
|
@@ -256,7 +405,7 @@ const useLocalStore = createStore(
|
|
|
256
405
|
{
|
|
257
406
|
state: { sessionCount: 0, lastVisited: new Date().toISOString() },
|
|
258
407
|
actions: {
|
|
259
|
-
incrementSessionCount: (state) => ({ sessionCount: state.sessionCount + 1 }),
|
|
408
|
+
incrementSessionCount: ({state}) => ({ sessionCount: state.sessionCount + 1 }),
|
|
260
409
|
updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
|
|
261
410
|
},
|
|
262
411
|
},
|
|
@@ -284,7 +433,7 @@ const useUserProfileStore = createStore(
|
|
|
284
433
|
state: { userId: '', preferences: { language: 'en', darkMode: false } },
|
|
285
434
|
actions: {
|
|
286
435
|
setUserId: (state, id: string) => ({ userId: id }),
|
|
287
|
-
toggleDarkMode: (state) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
436
|
+
toggleDarkMode: ({state}) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
288
437
|
},
|
|
289
438
|
},
|
|
290
439
|
{ persistence: indexedDBPersistence },
|
|
@@ -297,7 +446,7 @@ function AppWithPersistence() {
|
|
|
297
446
|
const sessionCount = selectLocal(s => s.sessionCount);
|
|
298
447
|
const darkMode = selectProfile(s => s.preferences.darkMode);
|
|
299
448
|
|
|
300
|
-
|
|
449
|
+
useEffect(() => {
|
|
301
450
|
if (localReady) {
|
|
302
451
|
actionsLocal.incrementSessionCount();
|
|
303
452
|
actionsLocal.updateLastVisited();
|
|
@@ -305,7 +454,7 @@ function AppWithPersistence() {
|
|
|
305
454
|
if (profileReady && !selectProfile(s => s.userId)) {
|
|
306
455
|
actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
|
|
307
456
|
}
|
|
308
|
-
}, [localReady, profileReady]);
|
|
457
|
+
}, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]); // Added dependencies for useEffect
|
|
309
458
|
|
|
310
459
|
if (!localReady || !profileReady) {
|
|
311
460
|
return <div>Loading persisted data...</div>;
|
|
@@ -326,10 +475,11 @@ function AppWithPersistence() {
|
|
|
326
475
|
|
|
327
476
|
### Middleware
|
|
328
477
|
|
|
329
|
-
Middleware functions can intercept and modify or block state updates. They are executed in the order they are added.
|
|
478
|
+
Middleware functions can intercept and modify or block state updates. They are executed in the order they are added. Transforming middleware can alter the state update payload, while blocking middleware can prevent an update from proceeding.
|
|
330
479
|
|
|
331
480
|
```typescript
|
|
332
|
-
import { createStore, Middleware, BlockingMiddleware } from '@asaidimu/react-store';
|
|
481
|
+
import { createStore, type Middleware, type BlockingMiddleware } from '@asaidimu/react-store';
|
|
482
|
+
import React from 'react';
|
|
333
483
|
|
|
334
484
|
interface CartState {
|
|
335
485
|
items: Array<{ id: string; name: string; quantity: number; price: number }>;
|
|
@@ -360,7 +510,7 @@ const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) =>
|
|
|
360
510
|
const useCartStore = createStore({
|
|
361
511
|
state: { items: [], total: 0 },
|
|
362
512
|
actions: {
|
|
363
|
-
addItem: (state, item: { id: string; name: string; price: number }) => {
|
|
513
|
+
addItem: ({state}, item: { id: string; name: string; price: number }) => {
|
|
364
514
|
const existingItem = state.items.find(i => i.id === item.id);
|
|
365
515
|
if (existingItem) {
|
|
366
516
|
return {
|
|
@@ -373,10 +523,11 @@ const useCartStore = createStore({
|
|
|
373
523
|
items: [...state.items, { ...item, quantity: 1 }],
|
|
374
524
|
};
|
|
375
525
|
},
|
|
376
|
-
updateQuantity: (state, id: string, quantity: number) => ({
|
|
526
|
+
updateQuantity: ({state}, id: string, quantity: number) => ({
|
|
377
527
|
items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
|
|
378
528
|
}),
|
|
379
529
|
},
|
|
530
|
+
// Middleware now accepts an object mapping names to middleware functions
|
|
380
531
|
middleware: { calculateTotal: calculateTotalMiddleware },
|
|
381
532
|
blockingMiddleware: { validateItem: validateItemMiddleware },
|
|
382
533
|
});
|
|
@@ -408,10 +559,11 @@ function CartComponent() {
|
|
|
408
559
|
|
|
409
560
|
### Observability
|
|
410
561
|
|
|
411
|
-
Enable metrics and debugging via the `
|
|
562
|
+
Enable metrics and debugging via the `observer` and `actionTracker` objects. The `enableMetrics` option in `createStore` is crucial for activating these features.
|
|
412
563
|
|
|
413
564
|
```tsx
|
|
414
565
|
import { createStore } from '@asaidimu/react-store';
|
|
566
|
+
import React from 'react';
|
|
415
567
|
|
|
416
568
|
const useObservedStore = createStore(
|
|
417
569
|
{
|
|
@@ -422,7 +574,7 @@ const useObservedStore = createStore(
|
|
|
422
574
|
},
|
|
423
575
|
},
|
|
424
576
|
{
|
|
425
|
-
enableMetrics: true, // Crucial for enabling the 'observer'
|
|
577
|
+
enableMetrics: true, // Crucial for enabling the 'observer' and 'actionTracker' objects
|
|
426
578
|
enableConsoleLogging: true, // Log events directly to browser console
|
|
427
579
|
logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
|
|
428
580
|
performanceThresholds: {
|
|
@@ -431,7 +583,7 @@ const useObservedStore = createStore(
|
|
|
431
583
|
},
|
|
432
584
|
maxEvents: 500, // Max number of events to keep in history
|
|
433
585
|
maxStateHistory: 50, // Max number of state snapshots for time travel
|
|
434
|
-
debounceTime:
|
|
586
|
+
debounceTime: 0, // Actions execute immediately by default
|
|
435
587
|
},
|
|
436
588
|
);
|
|
437
589
|
|
|
@@ -441,16 +593,16 @@ function DebugPanel() {
|
|
|
441
593
|
// Access performance metrics
|
|
442
594
|
const metrics = observer?.getPerformanceMetrics();
|
|
443
595
|
|
|
444
|
-
// Access state history for time travel
|
|
596
|
+
// Access state history for time travel (if maxStateHistory > 0)
|
|
445
597
|
const timeTravel = observer?.createTimeTravel();
|
|
446
598
|
|
|
447
599
|
// Access action execution history
|
|
448
|
-
const actionHistory = actionTracker
|
|
600
|
+
const actionHistory = actionTracker?.getExecutions() || []; // actionTracker is only available if enableMetrics is true
|
|
449
601
|
|
|
450
602
|
return (
|
|
451
603
|
<div>
|
|
452
604
|
<h2>Debug Panel</h2>
|
|
453
|
-
{observer && (
|
|
605
|
+
{observer && ( // Check if observer is enabled
|
|
454
606
|
<>
|
|
455
607
|
<h3>Performance Metrics</h3>
|
|
456
608
|
<p>Update Count: {metrics?.updateCount}</p>
|
|
@@ -481,47 +633,50 @@ function DebugPanel() {
|
|
|
481
633
|
|
|
482
634
|
### Remote Observability
|
|
483
635
|
|
|
484
|
-
Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring.
|
|
636
|
+
Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring. This functionality typically resides in the `@asaidimu/utils-store` ecosystem.
|
|
485
637
|
|
|
486
638
|
```tsx
|
|
487
|
-
import { createStore
|
|
639
|
+
import { createStore } from '@asaidimu/react-store';
|
|
640
|
+
import { useRemoteObservability } from '@asaidimu/utils-store';
|
|
488
641
|
import React, { useEffect } from 'react';
|
|
489
642
|
|
|
490
643
|
const useRemoteStore = createStore(
|
|
491
644
|
{
|
|
492
645
|
state: { apiCallsMade: 0, lastApiError: null },
|
|
493
646
|
actions: {
|
|
494
|
-
simulateApiCall: async (state) => {
|
|
495
|
-
// Simulate an error 10% of the time
|
|
647
|
+
simulateApiCall: async ({state}) => {
|
|
496
648
|
if (Math.random() < 0.1) {
|
|
497
649
|
throw new Error('API request failed');
|
|
498
650
|
}
|
|
499
651
|
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
|
|
500
652
|
},
|
|
501
|
-
handleApiError: (state, error: string) => ({ lastApiError: error })
|
|
653
|
+
handleApiError: ({state}, error: string) => ({ lastApiError: error })
|
|
502
654
|
},
|
|
503
655
|
},
|
|
504
656
|
{
|
|
505
657
|
enableMetrics: true, // Required for RemoteObservability
|
|
506
658
|
enableConsoleLogging: false,
|
|
507
|
-
collectCategories
|
|
508
|
-
|
|
509
|
-
errors: true,
|
|
510
|
-
stateChanges: true,
|
|
511
|
-
middleware: true,
|
|
512
|
-
},
|
|
513
|
-
reportingInterval: 10000, // Send metrics every 10 seconds
|
|
514
|
-
batchSize: 10, // Send after 10 metrics or interval, whichever comes first
|
|
515
|
-
immediateReporting: false, // Don't send immediately after each metric
|
|
659
|
+
// collectCategories configuration would typically be passed to useRemoteObservability
|
|
660
|
+
// reportingInterval, batchSize, immediateReporting would also be part of useRemoteObservability options
|
|
516
661
|
}
|
|
517
662
|
);
|
|
518
663
|
|
|
519
664
|
function MonitoringIntegration() {
|
|
520
665
|
const { store, observer } = useRemoteStore();
|
|
666
|
+
// useRemoteObservability is assumed to be part of utils-store or a separate utility
|
|
521
667
|
const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
|
|
522
668
|
serviceName: 'my-react-app',
|
|
523
669
|
environment: 'development',
|
|
524
670
|
instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
|
|
671
|
+
collectCategories: {
|
|
672
|
+
performance: true,
|
|
673
|
+
errors: true,
|
|
674
|
+
stateChanges: true,
|
|
675
|
+
middleware: true,
|
|
676
|
+
},
|
|
677
|
+
reportingInterval: 10000, // Send metrics every 10 seconds
|
|
678
|
+
batchSize: 10, // Send after 10 metrics or interval, whichever comes first
|
|
679
|
+
immediateReporting: false, // Don't send immediately after each metric
|
|
525
680
|
});
|
|
526
681
|
|
|
527
682
|
useEffect(() => {
|
|
@@ -552,24 +707,31 @@ function MonitoringIntegration() {
|
|
|
552
707
|
}, 5000); // Report every 5 seconds
|
|
553
708
|
|
|
554
709
|
return () => clearInterval(interval);
|
|
555
|
-
}, []);
|
|
710
|
+
}, [observer, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination]); // Added dependencies
|
|
556
711
|
|
|
557
712
|
return null; // This component doesn't render anything visually
|
|
558
713
|
}
|
|
559
714
|
|
|
560
|
-
// In your App component:
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
715
|
+
// In your App component, you would use it like:
|
|
716
|
+
// function MyApp() {
|
|
717
|
+
// return (
|
|
718
|
+
// <>
|
|
719
|
+
// <MonitoringIntegration />
|
|
720
|
+
// <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
|
|
721
|
+
// Simulate API Call
|
|
722
|
+
// </button>
|
|
723
|
+
// </>
|
|
724
|
+
// );
|
|
725
|
+
// }
|
|
565
726
|
```
|
|
566
727
|
|
|
567
728
|
### Transaction Support
|
|
568
729
|
|
|
569
|
-
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.
|
|
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.
|
|
570
731
|
|
|
571
732
|
```typescript
|
|
572
733
|
import { createStore } from '@asaidimu/react-store';
|
|
734
|
+
import React from 'react';
|
|
573
735
|
|
|
574
736
|
interface BankState {
|
|
575
737
|
checking: number;
|
|
@@ -580,7 +742,7 @@ interface BankState {
|
|
|
580
742
|
const useBankStore = createStore<BankState, any>({
|
|
581
743
|
state: { checking: 1000, savings: 500, transactions: [] },
|
|
582
744
|
actions: {
|
|
583
|
-
transferFunds: async (state, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
745
|
+
transferFunds: async ({state}, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
584
746
|
if (amount <= 0) {
|
|
585
747
|
throw new Error('Transfer amount must be positive.');
|
|
586
748
|
}
|
|
@@ -608,14 +770,15 @@ const useBankStore = createStore<BankState, any>({
|
|
|
608
770
|
});
|
|
609
771
|
|
|
610
772
|
function BankApp() {
|
|
611
|
-
const { select, actions } = useBankStore();
|
|
773
|
+
const { select, actions, store } = useBankStore();
|
|
612
774
|
const checkingBalance = select(s => s.checking);
|
|
613
775
|
const savingsBalance = select(s => s.savings);
|
|
614
776
|
const transactions = select(s => s.transactions);
|
|
615
777
|
|
|
616
778
|
const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
|
|
617
779
|
try {
|
|
618
|
-
|
|
780
|
+
// Wrap the action call in a store transaction
|
|
781
|
+
await store.transaction(() => actions.transferFunds(from, to, amount));
|
|
619
782
|
alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
|
|
620
783
|
} catch (error) {
|
|
621
784
|
alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -643,7 +806,7 @@ function BankApp() {
|
|
|
643
806
|
|
|
644
807
|
### Event System
|
|
645
808
|
|
|
646
|
-
The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects.
|
|
809
|
+
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()`.
|
|
647
810
|
|
|
648
811
|
```typescript
|
|
649
812
|
import { createStore } from '@asaidimu/react-store';
|
|
@@ -653,7 +816,7 @@ const useEventStore = createStore(
|
|
|
653
816
|
{
|
|
654
817
|
state: { data: 'initial', processedCount: 0 },
|
|
655
818
|
actions: {
|
|
656
|
-
processData: (state, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
819
|
+
processData: ({state}, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
657
820
|
triggerError: () => { throw new Error("Action failed intentionally"); }
|
|
658
821
|
},
|
|
659
822
|
middleware: {
|
|
@@ -666,7 +829,7 @@ const useEventStore = createStore(
|
|
|
666
829
|
);
|
|
667
830
|
|
|
668
831
|
function EventMonitor() {
|
|
669
|
-
const { store } = useEventStore();
|
|
832
|
+
const { store, actions } = useEventStore();
|
|
670
833
|
const [eventLogs, setEventLogs] = React.useState<string[]>([]);
|
|
671
834
|
|
|
672
835
|
useEffect(() => {
|
|
@@ -719,8 +882,6 @@ function EventMonitor() {
|
|
|
719
882
|
};
|
|
720
883
|
}, [store]); // Re-subscribe if store instance changes (unlikely)
|
|
721
884
|
|
|
722
|
-
const { actions } = useEventStore();
|
|
723
|
-
|
|
724
885
|
return (
|
|
725
886
|
<div>
|
|
726
887
|
<h3>Store Event Log</h3>
|
|
@@ -737,23 +898,84 @@ function EventMonitor() {
|
|
|
737
898
|
}
|
|
738
899
|
```
|
|
739
900
|
|
|
901
|
+
### Watching Action Loading States
|
|
902
|
+
|
|
903
|
+
The `watch` function returned by the `useStore` hook allows you to subscribe to the loading status of individual actions. This is particularly useful for displaying loading indicators for asynchronous operations.
|
|
904
|
+
|
|
905
|
+
```tsx
|
|
906
|
+
import React from 'react';
|
|
907
|
+
import { createStore } from '@asaidimu/react-store';
|
|
908
|
+
|
|
909
|
+
interface DataState {
|
|
910
|
+
items: string[];
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const useDataStore = createStore({
|
|
914
|
+
state: { items: [] },
|
|
915
|
+
actions: {
|
|
916
|
+
fetchItems: async (state) => {
|
|
917
|
+
// Simulate an API call
|
|
918
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
919
|
+
return { items: ['Item A', 'Item B', 'Item C'] };
|
|
920
|
+
},
|
|
921
|
+
addItem: async ({state}, item: string) => {
|
|
922
|
+
// Simulate a quick add operation
|
|
923
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
924
|
+
return { items: [...state.items, item] };
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
function DataLoader() {
|
|
930
|
+
const { actions, select, watch } = useDataStore();
|
|
931
|
+
const items = select(s => s.items);
|
|
932
|
+
|
|
933
|
+
// Watch the loading state of specific actions
|
|
934
|
+
const isFetchingItems = watch('fetchItems');
|
|
935
|
+
const isAddingItem = watch('addItem');
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
<div>
|
|
939
|
+
<h2>Data Loader</h2>
|
|
940
|
+
<button onClick={() => actions.fetchItems()} disabled={isFetchingItems}>
|
|
941
|
+
{isFetchingItems ? 'Fetching...' : 'Fetch Items'}
|
|
942
|
+
</button>
|
|
943
|
+
<button onClick={() => actions.addItem(`New Item ${items.length + 1}`)} disabled={isAddingItem}>
|
|
944
|
+
{isAddingItem ? 'Adding...' : 'Add Item'}
|
|
945
|
+
</button>
|
|
946
|
+
|
|
947
|
+
<h3>Items:</h3>
|
|
948
|
+
<ul>
|
|
949
|
+
{items.map((item, index) => (
|
|
950
|
+
<li key={index}>{item}</li>
|
|
951
|
+
))}
|
|
952
|
+
</ul>
|
|
953
|
+
</div>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
740
958
|
### Advanced Hook Properties
|
|
741
959
|
|
|
742
960
|
The hook returned by `createStore` provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
|
|
743
961
|
|
|
744
962
|
```tsx
|
|
963
|
+
import { useStore as useMyStore } from '../ui/store'; // Assuming this is your store definition
|
|
964
|
+
|
|
745
965
|
function MyAdvancedComponent() {
|
|
746
966
|
const {
|
|
747
|
-
select, // Function to select state parts (memoized)
|
|
748
|
-
actions, // Object containing your defined actions (debounced)
|
|
967
|
+
select, // Function to select state parts (memoized, reactive)
|
|
968
|
+
actions, // Object containing your defined actions (debounced, promise-returning)
|
|
749
969
|
isReady, // Boolean indicating if persistence is ready
|
|
750
|
-
store, // Direct access to the ReactiveDataStore instance
|
|
751
|
-
observer, // StoreObservability instance (if `enableMetrics` was true)
|
|
752
|
-
actionTracker, // Instance of ActionTracker for monitoring action executions
|
|
753
|
-
state, // A
|
|
970
|
+
store, // Direct access to the ReactiveDataStore instance (from @asaidimu/utils-store)
|
|
971
|
+
observer, // StoreObservability instance (from @asaidimu/utils-store, if `enableMetrics` was true)
|
|
972
|
+
actionTracker, // Instance of ActionTracker for monitoring action executions (if `enableMetrics` was true)
|
|
973
|
+
state, // A getter function `() => TState` to get the entire current state object (reactive)
|
|
974
|
+
watch, // Function to watch the loading status of actions
|
|
975
|
+
resolve, // Function to resolve an artifact (if artifacts are defined)
|
|
754
976
|
} = useMyStore(); // Assuming useMyStore is defined from createStore
|
|
755
977
|
|
|
756
|
-
// Example: Accessing the full state (use with caution for performance
|
|
978
|
+
// Example: Accessing the full state (use with caution for performance; `select` is preferred)
|
|
757
979
|
const fullCurrentState = state();
|
|
758
980
|
console.log("Full reactive state:", fullCurrentState);
|
|
759
981
|
|
|
@@ -764,11 +986,24 @@ function MyAdvancedComponent() {
|
|
|
764
986
|
}
|
|
765
987
|
|
|
766
988
|
// Example: Accessing action history
|
|
767
|
-
|
|
989
|
+
if (actionTracker) { // actionTracker is only available if enableMetrics is true
|
|
990
|
+
console.log("Action executions:", actionTracker.getExecutions());
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Example: Watching a specific action's loading state
|
|
994
|
+
const isLoadingSomeAction = watch('checkout'); // Assuming 'checkout' is an action
|
|
995
|
+
console.log("Is 'checkout' action loading?", isLoadingSomeAction);
|
|
996
|
+
|
|
997
|
+
// Example: Resolving an artifact (if defined in your store)
|
|
998
|
+
// const [myArtifact, isArtifactReady] = resolve('myArtifactKey');
|
|
999
|
+
// if (isArtifactReady) {
|
|
1000
|
+
// console.log("My artifact is ready:", myArtifact);
|
|
1001
|
+
// }
|
|
768
1002
|
|
|
769
1003
|
return (
|
|
770
1004
|
<div>
|
|
771
1005
|
{/* ... your component content ... */}
|
|
1006
|
+
<p>Store Ready: {isReady ? 'Yes' : 'No'}</p>
|
|
772
1007
|
</div>
|
|
773
1008
|
);
|
|
774
1009
|
}
|
|
@@ -776,65 +1011,60 @@ function MyAdvancedComponent() {
|
|
|
776
1011
|
|
|
777
1012
|
## Project Architecture
|
|
778
1013
|
|
|
779
|
-
`@asaidimu/react-store` is structured to provide a modular yet integrated state management solution.
|
|
1014
|
+
`@asaidimu/react-store` is structured to provide a modular yet integrated state management solution. It separates core state logic into reusable utilities while offering a streamlined React-specific API.
|
|
1015
|
+
|
|
1016
|
+
### Internal Structure
|
|
1017
|
+
|
|
1018
|
+
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.
|
|
780
1019
|
|
|
781
1020
|
```
|
|
782
1021
|
.
|
|
783
1022
|
├── src/
|
|
784
|
-
│ ├──
|
|
785
|
-
│
|
|
786
|
-
│
|
|
787
|
-
|
|
788
|
-
│ │ ├── local-storage.ts # WebStorage (localStorage/sessionStorage) persistence adapter
|
|
789
|
-
│ │ └── types.ts # Interface for persistence adapters
|
|
790
|
-
│ ├── state/
|
|
791
|
-
│ │ ├── diff.ts # Utility for deep diffing state objects
|
|
792
|
-
│ │ ├── merge.ts # Utility for immutable deep merging state objects
|
|
793
|
-
│ │ ├── observability.ts # Core observability logic for ReactiveDataStore
|
|
794
|
-
│ │ └── store.ts # Core ReactiveDataStore implementation (the state machine)
|
|
795
|
-
│ ├── store/
|
|
796
|
-
│ │ ├── compare.ts # Utilities for fast comparison (e.g., array hashing)
|
|
797
|
-
│ │ ├── execution.ts # Action tracking interface and class
|
|
798
|
-
│ │ ├── hash.ts # Utilities for hashing objects (e.g., for selectors)
|
|
799
|
-
│ │ ├── index.ts # Main `createStore` React hook
|
|
800
|
-
│ │ ├── paths.ts # Utility for building selector paths
|
|
801
|
-
│ │ └── selector.ts # Selector memoization manager
|
|
802
|
-
│ ├── types.ts # Core TypeScript types for the library
|
|
803
|
-
│ └── utils/
|
|
804
|
-
│ ├── destinations.ts # Concrete remote observability destinations (OTel, Prometheus, Grafana)
|
|
805
|
-
│ └── remote-observability.ts # Remote observability extension for StoreObservability
|
|
806
|
-
├── index.ts # Main entry point for the library
|
|
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.
|
|
807
1027
|
├── package.json
|
|
808
1028
|
└── tsconfig.json
|
|
809
1029
|
```
|
|
810
1030
|
|
|
1031
|
+
### Key External Dependencies
|
|
1032
|
+
|
|
1033
|
+
This library leverages the following `@asaidimu` packages for its core functionalities:
|
|
1034
|
+
|
|
1035
|
+
* **`@asaidimu/utils-store`**: Provides the foundational `ReactiveDataStore` for immutable state management, transactions, core event emission, and the `StoreObservability` instance for deep insights into state changes.
|
|
1036
|
+
* **`@asaidimu/utils-persistence`**: Offers various persistence adapters like `WebStoragePersistence` and `IndexedDBPersistence` for saving and loading state, including cross-tab synchronization.
|
|
1037
|
+
|
|
811
1038
|
### Core Components
|
|
812
1039
|
|
|
813
|
-
* **`ReactiveDataStore` (
|
|
814
|
-
* **`StoreObservability` (
|
|
815
|
-
* **`
|
|
816
|
-
*
|
|
817
|
-
*
|
|
1040
|
+
* **`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
|
+
* **`StoreObservability` (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
|
+
* **`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
|
+
* **`createStore` Hook (`src/store.ts`)**: The primary React-facing API. It instantiates `ReactiveDataStore`, `StoreObservability`, and `ActionTracker`. It wraps user-defined actions with debouncing and tracking, and provides the `select` function (powered by `useSyncExternalStore` for efficient component updates) and the `watch` function for action loading states.
|
|
1044
|
+
* **Persistence Adapters (from `@asaidimu/utils-persistence`)**: Implement the `DataStorePersistence` interface. `WebStoragePersistence` (for `localStorage`/`sessionStorage`) and `IndexedDBPersistence` provide concrete, ready-to-use storage solutions with cross-tab synchronization capabilities.
|
|
818
1045
|
|
|
819
1046
|
### Data Flow
|
|
820
1047
|
|
|
821
|
-
1. **Action Dispatch**: A React component calls
|
|
822
|
-
2. **Action
|
|
823
|
-
3. **State Update
|
|
824
|
-
4. **
|
|
825
|
-
5. **
|
|
826
|
-
6. **
|
|
827
|
-
7. **
|
|
828
|
-
8. **
|
|
829
|
-
9. **
|
|
830
|
-
10. **
|
|
831
|
-
11. **
|
|
1048
|
+
1. **Action Dispatch**: A React component calls a bound action (e.g., `actions.addItem()`).
|
|
1049
|
+
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`.
|
|
1051
|
+
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.
|
|
1053
|
+
6. **Transaction Context**: If the action is wrapped within `store.transaction()`, the current state is snapshotted to enable potential rollback.
|
|
1054
|
+
7. **Blocking Middleware**: The update first passes through any registered `blockingMiddleware`. If any middleware returns `false` or throws an error, the update is halted, and the state remains unchanged (and rolled back if in a transaction).
|
|
1055
|
+
8. **Transform Middleware**: If not blocked, the update then passes through transforming `middleware`. These functions can modify the partial update payload.
|
|
1056
|
+
9. **State Merging**: The final, possibly transformed, update is immutably merged into the current state using `ReactiveDataStore`'s internal utility.
|
|
1057
|
+
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 `DataStorePersistence` adapter (e.g., `localStorage`, `IndexedDB`). The system also subscribes to external changes from persistence for cross-tab synchronization.
|
|
1059
|
+
12. **Listener Notification**: `React.useSyncExternalStore` subscribers (used by `select`) whose selected paths have changed are notified, triggering efficient re-renders of only the relevant components.
|
|
1060
|
+
13. **Action Loading State Reset**: Once the action completes (either successfully or with an error), the loading state for that action is reset to `false`.
|
|
1061
|
+
14. **Observability Events**: Throughout this entire flow, `ReactiveDataStore` emits fine-grained events (`update:start`, `middleware:complete`, `transaction:error`, etc.) which `StoreObservability` captures for debugging, metrics collection, and remote reporting.
|
|
832
1062
|
|
|
833
1063
|
### Extension Points
|
|
834
1064
|
|
|
835
|
-
* **Custom Middleware**: Easily add your own `Middleware` or `BlockingMiddleware` functions for custom logic.
|
|
836
|
-
* **Custom Persistence Adapters**: Implement the `DataStorePersistence<T>` interface to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store).
|
|
837
|
-
* **Remote Observability Destinations**: Create new `RemoteDestination` implementations to send metrics and traces to any external observability platform not already supported.
|
|
1065
|
+
* **Custom Middleware**: Easily add your own `Middleware` or `BlockingMiddleware` functions for custom logic (e.g., advanced logging, validation, side effects).
|
|
1066
|
+
* **Custom Persistence Adapters**: Implement the `DataStorePersistence<T>` interface (from `@asaidimu/utils-persistence`) to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store).
|
|
1067
|
+
* **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.
|
|
838
1068
|
|
|
839
1069
|
## Development & Contributing
|
|
840
1070
|
|
|
@@ -844,7 +1074,7 @@ We welcome contributions! Please follow the guidelines below.
|
|
|
844
1074
|
|
|
845
1075
|
1. **Clone the repository:**
|
|
846
1076
|
```bash
|
|
847
|
-
git clone https://github.com/asaidimu/react
|
|
1077
|
+
git clone https://github.com/asaidimu/node-react.git
|
|
848
1078
|
cd react-store
|
|
849
1079
|
```
|
|
850
1080
|
2. **Install dependencies:**
|
|
@@ -855,18 +1085,18 @@ We welcome contributions! Please follow the guidelines below.
|
|
|
855
1085
|
|
|
856
1086
|
### Scripts
|
|
857
1087
|
|
|
858
|
-
* `bun ci`: Installs dependencies
|
|
859
|
-
* `bun test`: Runs all unit tests using `Vitest
|
|
860
|
-
* `bun test:ci`: Runs tests
|
|
861
|
-
* `bun clean`: Removes the `dist` directory.
|
|
862
|
-
* `bun prebuild`:
|
|
863
|
-
* `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies
|
|
864
|
-
* `bun dev`: Starts
|
|
865
|
-
* `bun postbuild`:
|
|
1088
|
+
* `bun ci`: Installs dependencies, typically used in CI/CD environments to ensure a clean install.
|
|
1089
|
+
* `bun test`: Runs all unit tests using `Vitest` in interactive watch mode.
|
|
1090
|
+
* `bun test:ci`: Runs all unit tests once, suitable for CI/CD pipelines.
|
|
1091
|
+
* `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.
|
|
1093
|
+
* `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies the code using `Terser`.
|
|
1094
|
+
* `bun dev`: Starts the e-commerce dashboard demonstration application using `Vite`.
|
|
1095
|
+
* `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.
|
|
866
1096
|
|
|
867
1097
|
### Testing
|
|
868
1098
|
|
|
869
|
-
Tests are written using `Vitest` and `React Testing Library
|
|
1099
|
+
Tests are written using `Vitest` and `React Testing Library` for component and hook testing.
|
|
870
1100
|
|
|
871
1101
|
To run tests:
|
|
872
1102
|
|
|
@@ -880,116 +1110,103 @@ bun test --watch
|
|
|
880
1110
|
|
|
881
1111
|
1. **Fork** the repository and create your branch from `main`.
|
|
882
1112
|
2. **Code Standards**: Ensure your code adheres to existing coding styles (TypeScript, ESLint, Prettier are configured).
|
|
883
|
-
3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all tests pass.
|
|
884
|
-
4. **Commits**: Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
|
|
885
|
-
5. **Pull Requests**: Submit a pull request to the `main` branch. Provide a clear description of your changes.
|
|
1113
|
+
3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all tests pass (`bun test`).
|
|
1114
|
+
4. **Commits**: Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. This project uses `semantic-release` for automated versioning and changelog generation.
|
|
1115
|
+
5. **Pull Requests**: Submit a pull request to the `main` branch. Provide a clear description of your changes, referencing any relevant issues.
|
|
886
1116
|
|
|
887
1117
|
### Issue Reporting
|
|
888
1118
|
|
|
889
|
-
For bugs, feature requests, or questions, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/react
|
|
1119
|
+
For bugs, feature requests, or questions, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/node-react/issues).
|
|
890
1120
|
|
|
891
1121
|
## Additional Information
|
|
892
1122
|
|
|
893
1123
|
### Best Practices
|
|
894
1124
|
|
|
895
|
-
1. **Granular Selectors**: Always use `select((state) => state.path.to.value)` instead of `select((state) => state)` to prevent unnecessary re-renders of components.
|
|
896
|
-
2. **Action Design**: Keep actions focused on a single responsibility. Use `async` actions for asynchronous operations and return partial updates upon completion.
|
|
1125
|
+
1. **Granular Selectors**: Always use `select((state) => state.path.to.value)` instead of `select((state) => state)` to prevent unnecessary re-renders of components. The more specific your selector, the more optimized your component updates will be.
|
|
1126
|
+
2. **Action Design**: Keep actions focused on a single responsibility. Use `async` actions for asynchronous operations (e.g., API calls) and return partial updates or promises resolving to partial updates upon completion. Actions should describe *what* happened, not *how*.
|
|
897
1127
|
3. **Persistence**:
|
|
898
1128
|
* Use unique `storeId` or `storageKey` for each distinct store to avoid data conflicts.
|
|
899
|
-
* Always check the `isReady` flag for UI elements that depend on the initial state loaded from persistence.
|
|
900
|
-
4. **Middleware**: Leverage middleware for cross-cutting concerns like logging, analytics, or complex validation logic.
|
|
901
|
-
5. **`Symbol.for("delete")`**: Use this explicit symbol for property removal to maintain clarity and avoid accidental data mutations.
|
|
1129
|
+
* 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 middleware for cross-cutting concerns like logging, analytics, data transformation, or complex validation logic that applies to multiple actions.
|
|
1131
|
+
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
|
+
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.
|
|
902
1133
|
|
|
903
1134
|
### API Reference
|
|
904
1135
|
|
|
905
1136
|
#### `createStore(definition, options)`
|
|
906
1137
|
|
|
907
|
-
The main entry point for creating a store.
|
|
1138
|
+
The main entry point for creating a store hook.
|
|
908
1139
|
|
|
909
1140
|
```typescript
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1141
|
+
// From src/types.ts
|
|
1142
|
+
export interface StoreDefinition<
|
|
1143
|
+
TState extends object,
|
|
1144
|
+
TArtifactsMap extends ArtifactsMap<TState>, // Artifacts can be defined but not shown in this example
|
|
1145
|
+
TActions extends ActionMap<TState, TArtifactsMap>,
|
|
1146
|
+
> {
|
|
1147
|
+
state: TState;
|
|
1148
|
+
actions: TActions;
|
|
1149
|
+
artifacts?: TArtifactsMap; // Optional artifact definitions
|
|
1150
|
+
blockingMiddleware?: Record<string, BlockingMiddleware<TState>>; // Optional blocking middleware
|
|
1151
|
+
middleware?: Record<string, Middleware<TState>>; // Optional transforming middleware
|
|
1152
|
+
}
|
|
916
1153
|
|
|
917
|
-
interface StoreOptions<T> {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
logEvents?: { // Which event categories to log (defaults to all true if enableConsoleLogging is true)
|
|
923
|
-
updates?: boolean;
|
|
924
|
-
middleware?: boolean;
|
|
925
|
-
transactions?: boolean;
|
|
926
|
-
};
|
|
927
|
-
performanceThresholds?: { // Thresholds for logging slow operations (in ms)
|
|
928
|
-
updateTime?: number; // default: 50ms
|
|
929
|
-
middlewareTime?: number; // default: 20ms
|
|
930
|
-
};
|
|
931
|
-
persistence?: DataStorePersistence<T>; // Optional persistence adapter instance
|
|
932
|
-
debounceTime?: number; // Time in milliseconds to debounce actions (default: 250ms)
|
|
1154
|
+
interface StoreOptions<T> extends ObserverOptions { // ObserverOptions from @asaidimu/utils-store
|
|
1155
|
+
enableMetrics?: boolean; // Enable StoreObservability and ActionTracker features (default: false)
|
|
1156
|
+
persistence?: SimplePersistence<T>; // Optional persistence adapter instance (from @asaidimu/utils-persistence)
|
|
1157
|
+
debounceTime?: number; // Time in milliseconds to debounce actions (default: 0ms)
|
|
1158
|
+
// Other ObserverOptions for logging, performanceThresholds, maxEvents, maxStateHistory are inherited
|
|
933
1159
|
}
|
|
934
1160
|
|
|
935
|
-
const
|
|
1161
|
+
const useStoreHook = createStore(definition, options);
|
|
936
1162
|
```
|
|
937
1163
|
|
|
938
|
-
**Returns**: A `
|
|
939
|
-
|
|
940
|
-
* `
|
|
941
|
-
* `
|
|
942
|
-
* `
|
|
943
|
-
* `
|
|
944
|
-
* `
|
|
1164
|
+
**Returns**: A React hook (`useStoreHook`) which, when called in a component, returns an object with the following properties:
|
|
1165
|
+
|
|
1166
|
+
* `store`: Direct access to the underlying `ReactiveDataStore` instance (from `@asaidimu/utils-store`). This provides low-level control and event subscription.
|
|
1167
|
+
* `observer`: The `StoreObservability` instance (from `@asaidimu/utils-store`). Available only if `enableMetrics` is `true`. Provides debug, time-travel, and monitoring utilities.
|
|
1168
|
+
* `select`: A memoized selector function `(<S>(selector: (state: TState) => S) => S)`. Extracts specific state slices. Re-renders components only when selected data changes.
|
|
1169
|
+
* `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
|
+
* `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
|
+
* `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.
|
|
945
1172
|
* `isReady`: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
|
|
1173
|
+
* `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) => readonly [ExtractInstanceFromMap<TArtifactsMap, K>, true] | readonly [ExtractInstanceFromMap<TArtifactsMap, K> | undefined, false])`. If `artifacts` are defined in the store, this hook returns `[instance, isReady]` for a specific artifact, reactively updating if the artifact instance changes or becomes ready.
|
|
946
1175
|
|
|
947
|
-
#### `ReactiveDataStore` (accessed via `
|
|
1176
|
+
#### `ReactiveDataStore` (accessed via `useStoreHook().store` from `@asaidimu/utils-store`)
|
|
948
1177
|
|
|
949
|
-
* `get(clone?: boolean):
|
|
950
|
-
* `set(update: StateUpdater<
|
|
951
|
-
* `subscribe(path: string | string[], listener: (state:
|
|
952
|
-
* `transaction<R>(operation: () => R | Promise<R>): Promise<R>`: Executes a function as an atomic transaction. Rolls back all changes if an error occurs.
|
|
953
|
-
* `use(middleware: Middleware<
|
|
954
|
-
* `useBlockingMiddleware(middleware: BlockingMiddleware<
|
|
1178
|
+
* `get(clone?: boolean): TState`: Retrieves the current state. Pass `true` to get a deep clone (recommended for mutations outside of actions).
|
|
1179
|
+
* `set(update: StateUpdater<TState>): Promise<void>`: Updates the state with a partial object or a function returning a partial object.
|
|
1180
|
+
* `subscribe(path: string | string[], listener: (state: TState) => void): () => void`: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.
|
|
1181
|
+
* `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: Middleware<TState>, name?: string): string`: Adds a transforming middleware. Returns its ID.
|
|
1183
|
+
* `useBlockingMiddleware(middleware: BlockingMiddleware<TState>, name?: string): string`: Adds a blocking middleware. Returns its ID.
|
|
955
1184
|
* `removeMiddleware(id: string): boolean`: Removes a middleware by its ID.
|
|
956
1185
|
* `isReady(): boolean`: Checks if the persistence layer has loaded its initial state.
|
|
957
|
-
* `onStoreEvent(event: StoreEvent, listener: (data: any) => void): () => void`: Subscribes to internal store events (e.g., `'update:complete'`, `'middleware:error'`).
|
|
1186
|
+
* `onStoreEvent(event: StoreEvent, listener: (data: any) => void): () => void`: Subscribes to internal store events (e.g., `'update:complete'`, `'middleware:error'`, `'transaction:start'`).
|
|
1187
|
+
|
|
1188
|
+
#### `StoreObservability` (accessed via `useStoreHook().observer` from `@asaidimu/utils-store`)
|
|
958
1189
|
|
|
959
|
-
|
|
1190
|
+
Available only if `enableMetrics` is `true` in `createStore` options.
|
|
960
1191
|
|
|
961
1192
|
* `getEventHistory(): DebugEvent[]`: Retrieves a history of all captured store events.
|
|
962
|
-
* `getStateHistory():
|
|
963
|
-
* `getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from:
|
|
1193
|
+
* `getStateHistory(): TState[]`: Returns a history of state snapshots, enabling time-travel debugging (if `maxStateHistory` > 0).
|
|
1194
|
+
* `getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: DeepPartial<TState>; to: DeepPartial<TState>; }>`: Provides a simplified view of recent state changes.
|
|
964
1195
|
* `getPerformanceMetrics(): StoreMetrics`: Returns an object containing performance statistics (e.g., `updateCount`, `averageUpdateTime`).
|
|
965
1196
|
* `createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }`: Returns controls for time-travel debugging.
|
|
966
|
-
* `createLoggingMiddleware(options?: object): Middleware<
|
|
967
|
-
* `createValidationMiddleware(validator: (state:
|
|
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.
|
|
968
1199
|
* `clearHistory(): void`: Clears the event and state history.
|
|
969
1200
|
* `disconnect(): void`: Cleans up all listeners and resources.
|
|
970
1201
|
|
|
971
|
-
####
|
|
972
|
-
|
|
973
|
-
Extends `StoreObservability` with methods for sending metrics and traces externally.
|
|
974
|
-
|
|
975
|
-
* `addDestination(destination: RemoteDestination): boolean`: Adds a remote destination for metrics.
|
|
976
|
-
* `removeDestination(id: string): boolean`: Removes a remote destination by ID.
|
|
977
|
-
* `getDestinations(): Array<{ id: string; name: string }> `: Gets a list of configured destinations.
|
|
978
|
-
* `testAllConnections(): Promise<Record<string, boolean>>`: Tests connectivity to all destinations.
|
|
979
|
-
* `beginTrace(name: string): string`: Starts a new performance trace, returning its ID.
|
|
980
|
-
* `beginSpan(traceId: string, name: string, labels?: Record<string, string>): string`: Starts a new span within a trace, returning its ID.
|
|
981
|
-
* `endSpan(traceId: string, spanName: string): void`: Ends a specific span within a trace.
|
|
982
|
-
* `endTrace(traceId: string): void`: Ends a performance trace and sends it to remote destinations.
|
|
983
|
-
* `trackMetric(metric: RemoteMetricsPayload['metrics'][0]): void`: Manually add a metric to the batch for reporting.
|
|
1202
|
+
#### Persistence Adapters (from `@asaidimu/utils-persistence`)
|
|
984
1203
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
All adapters implement `DataStorePersistence<T>`:
|
|
1204
|
+
All adapters implement `SimplePersistence<T>`:
|
|
988
1205
|
|
|
989
1206
|
* `set(id:string, state: T): boolean | Promise<boolean>`: Persists data.
|
|
990
|
-
* `get(): T | null | Promise<T | null>`: Retrieves data.
|
|
991
|
-
* `subscribe(id:string, callback: (state:T) => void): () => void`: Subscribes to external changes.
|
|
992
|
-
* `clear(): boolean | Promise<boolean>`: Clears persisted data.
|
|
1207
|
+
* `get(id:string): T | null | Promise<T | null>`: Retrieves data.
|
|
1208
|
+
* `subscribe(id:string, callback: (state:T) => void): () => void`: Subscribes to external changes (e.g., from other tabs).
|
|
1209
|
+
* `clear(id:string): boolean | Promise<boolean>`: Clears persisted data.
|
|
993
1210
|
|
|
994
1211
|
##### `IndexedDBPersistence(storeId: string)`
|
|
995
1212
|
|
|
@@ -1000,13 +1217,9 @@ All adapters implement `DataStorePersistence<T>`:
|
|
|
1000
1217
|
* **`storageKey`**: The key under which data is stored (e.g., `'app-config'`).
|
|
1001
1218
|
* **`session`**: Optional. If `true`, uses `sessionStorage`; otherwise, uses `localStorage` (default: `false`).
|
|
1002
1219
|
|
|
1003
|
-
##### `LocalStoragePersistence(storageKey: string)` (Deprecated)
|
|
1004
|
-
|
|
1005
|
-
* This is an alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
|
|
1006
|
-
|
|
1007
1220
|
### Comparison with Other State Management Solutions
|
|
1008
1221
|
|
|
1009
|
-
`@asaidimu/react-store` aims to
|
|
1222
|
+
`@asaidimu/react-store` aims to be a comprehensive, all-in-one solution for React state management, integrating features that often require multiple libraries in other ecosystems. Here's a comparison to popular alternatives:
|
|
1010
1223
|
|
|
1011
1224
|
| **Feature** | **@asaidimu/react-store** | **Redux** | **Zustand** | **MobX** | **Recoil** |
|
|
1012
1225
|
| :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
|
|
@@ -1015,20 +1228,59 @@ All adapters implement `DataStorePersistence<T>`:
|
|
|
1015
1228
|
| **API Complexity** | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
|
|
1016
1229
|
| **Scalability** | High (transactions, persistence, remote metrics). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
|
|
1017
1230
|
| **Extensibility** | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
|
|
1018
|
-
| **Performance** | Optimized (selectors, reactive updates). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
|
|
1019
|
-
| **Bundle Size** | Moderate (includes observability, persistence, remote observability). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
|
|
1231
|
+
| **Performance** | Optimized (selectors, reactive updates via `useSyncExternalStore`). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
|
|
1232
|
+
| **Bundle Size** | Moderate (includes observability, persistence, remote observability framework). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
|
|
1020
1233
|
| **Persistence** | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
|
|
1021
|
-
| **Observability** | Excellent (metrics, time-travel, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
|
|
1022
|
-
| **React Integration** | Native (hooks, `useSyncExternalStore`) | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
|
|
1234
|
+
| **Observability** | Excellent (metrics, time-travel, event logging, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
|
|
1235
|
+
| **React Integration** | Native (hooks, `useSyncExternalStore`). | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
|
|
1023
1236
|
|
|
1024
1237
|
#### Where `@asaidimu/react-store` Shines
|
|
1025
|
-
* **All-in-One**: It aims to be a single solution for state management, persistence, and observability, reducing the need for multiple external dependencies.
|
|
1026
|
-
* **Flexibility**: The robust middleware system
|
|
1027
|
-
* **Modern React**: It leverages `useSyncExternalStore` for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders.
|
|
1238
|
+
* **All-in-One**: It aims to be a single solution for state management, persistence, and observability, reducing the need for multiple external dependencies and their integration complexities.
|
|
1239
|
+
* **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
|
+
* **Modern React**: It leverages `useSyncExternalStore` for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders with minimal overhead.
|
|
1028
1241
|
|
|
1029
1242
|
#### Trade-Offs
|
|
1030
|
-
* **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.
|
|
1031
|
-
* **Learning Curve**: The rich feature set might present a slightly steeper learning curve for developers new to advanced state management
|
|
1243
|
+
* **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.
|
|
1245
|
+
|
|
1246
|
+
### Troubleshooting
|
|
1247
|
+
|
|
1248
|
+
* **Components not re-rendering:**
|
|
1249
|
+
* Ensure you are using `select` with a specific path (e.g., `select(s => s.user.name)`) instead of the entire state object.
|
|
1250
|
+
* Verify that the data at the selected path is actually changing (reference equality matters for objects/arrays).
|
|
1251
|
+
* **Persistence not loading/saving:**
|
|
1252
|
+
* Check if `isReady` is `true` before interacting with state dependent on persistence.
|
|
1253
|
+
* Ensure your `persistence` instance is correctly passed to `createStore`.
|
|
1254
|
+
* Check browser developer tools for `localStorage`, `sessionStorage`, or `IndexedDB` to see if data is being stored/retrieved.
|
|
1255
|
+
* Look for console errors related to persistence.
|
|
1256
|
+
* **Actions not executing or seem delayed:**
|
|
1257
|
+
* Check the `debounceTime` option in your `createStore` configuration. If set, actions will be delayed.
|
|
1258
|
+
* If actions are `async`, ensure you `await` them where necessary in your components.
|
|
1259
|
+
* Use the `watch` function to check if an action is in a loading state.
|
|
1260
|
+
* **Middleware not working:**
|
|
1261
|
+
* Ensure middleware functions are correctly registered in the `middleware` or `blockingMiddleware` objects in `createStore`.
|
|
1262
|
+
* Check console logs (if `enableConsoleLogging` is `true`) for `middleware:start`, `middleware:complete`, or `middleware:error` events.
|
|
1263
|
+
* **TypeScript errors:**
|
|
1264
|
+
* Verify your state interfaces match the `initialState` structure.
|
|
1265
|
+
* Ensure action return types are `DeepPartial<TState>` as expected.
|
|
1266
|
+
* Update `@asaidimu/react-store` and `@types/react` to their latest compatible versions.
|
|
1267
|
+
|
|
1268
|
+
### FAQ
|
|
1269
|
+
|
|
1270
|
+
**Q: Does it support React Server Components (RSC)?**
|
|
1271
|
+
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
|
+
|
|
1273
|
+
**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.
|
|
1275
|
+
|
|
1276
|
+
**Q: Can I use it without React?**
|
|
1277
|
+
A: The core state management (`ReactiveDataStore`, `StoreObservability` 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
|
+
|
|
1279
|
+
**Q: What about immutability?**
|
|
1280
|
+
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
|
+
|
|
1282
|
+
**Q: Is there a dev tools extension?**
|
|
1283
|
+
A: Currently, there is no dedicated browser extension. However, the built-in `StoreObservability` (`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.
|
|
1032
1284
|
|
|
1033
1285
|
### Changelog
|
|
1034
1286
|
|
|
@@ -1041,3 +1293,4 @@ This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.m
|
|
|
1041
1293
|
### Acknowledgments
|
|
1042
1294
|
|
|
1043
1295
|
Developed by [Saidimu](https://github.com/asaidimu).
|
|
1296
|
+
This library leverages the robust capabilities of [@asaidimu/utils-store](https://github.com/asaidimu/utils-store) and [@asaidimu/utils-persistence](https://github.com/asaidimu/utils-persistence) for its core state management and persistence features.
|