@asaidimu/react-store 2.1.0 → 4.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 +446 -237
- package/index.cjs +1 -1
- package/index.d.cts +55 -69
- package/index.d.ts +55 -69
- package/index.js +1 -1
- package/package.json +2 -2
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
|
|
|
@@ -21,6 +22,7 @@ This package is currently in **beta**. The API is subject to rapid changes and s
|
|
|
21
22
|
* [Persistence](#persistence)
|
|
22
23
|
* [Middleware](#middleware)
|
|
23
24
|
* [Observability](#observability)
|
|
25
|
+
* [Remote Observability](#remote-observability)
|
|
24
26
|
* [Transaction Support](#transaction-support)
|
|
25
27
|
* [Event System](#event-system)
|
|
26
28
|
* [Watching Action Loading States](#watching-action-loading-states)
|
|
@@ -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.
|
|
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.
|
|
54
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.
|
|
55
59
|
* 🚀 **Action Loading States**: Track the real-time loading status of individual actions, providing immediate feedback on asynchronous operations.
|
|
56
|
-
* ⚛️ **React
|
|
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,98 +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
|
|
|
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');
|
|
97
113
|
```
|
|
98
114
|
|
|
99
|
-
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.
|
|
100
116
|
|
|
101
117
|
## Usage Documentation
|
|
102
118
|
|
|
103
119
|
### Creating a Store
|
|
104
120
|
|
|
105
|
-
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.
|
|
106
122
|
|
|
107
123
|
```tsx
|
|
108
|
-
//
|
|
109
|
-
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
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CartItem extends Product {
|
|
136
|
+
quantity: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface Order {
|
|
140
|
+
id: string;
|
|
141
|
+
items: CartItem[];
|
|
142
|
+
total: number;
|
|
143
|
+
date: Date;
|
|
144
|
+
}
|
|
110
145
|
|
|
111
|
-
interface
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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;
|
|
115
152
|
}
|
|
116
153
|
|
|
117
|
-
const initialState:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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,
|
|
121
172
|
};
|
|
122
173
|
|
|
123
|
-
const
|
|
124
|
-
state:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}),
|
|
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 }] };
|
|
135
185
|
},
|
|
136
|
-
}, {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
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
|
+
};
|
|
214
|
+
},
|
|
215
|
+
|
|
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
|
+
};
|
|
142
239
|
|
|
143
|
-
|
|
144
|
-
|
|
240
|
+
export const useStore = createStore(
|
|
241
|
+
{
|
|
242
|
+
state: initialState,
|
|
243
|
+
actions,
|
|
244
|
+
},
|
|
245
|
+
{ enableMetrics: true } // Enables metrics for observability
|
|
246
|
+
);
|
|
145
247
|
```
|
|
146
248
|
|
|
147
249
|
### Using in Components
|
|
148
250
|
|
|
149
|
-
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.
|
|
150
252
|
|
|
151
253
|
```tsx
|
|
152
|
-
//
|
|
153
|
-
import
|
|
154
|
-
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
|
|
155
258
|
|
|
156
|
-
|
|
157
|
-
const { select, actions, isReady } = useMyStore();
|
|
259
|
+
// ... (ShadCN-like UI Components for brevity) ...
|
|
158
260
|
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const theme = select((state) => state.settings.theme);
|
|
261
|
+
const ProductCatalog = () => {
|
|
262
|
+
const { select, actions } = useStore();
|
|
263
|
+
const products = select((state) => state.products); // Granular selection
|
|
163
264
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
303
|
|
|
169
304
|
return (
|
|
170
|
-
<div
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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>
|
|
177
325
|
</div>
|
|
178
326
|
);
|
|
179
327
|
}
|
|
180
328
|
|
|
181
|
-
export default
|
|
329
|
+
export default App;
|
|
182
330
|
```
|
|
183
331
|
|
|
184
332
|
### Handling Deletions
|
|
@@ -202,12 +350,12 @@ const deleteStore = createStore({
|
|
|
202
350
|
tags: ['electronics', 'new']
|
|
203
351
|
},
|
|
204
352
|
actions: {
|
|
205
|
-
removeDetails: (
|
|
206
|
-
removeDimensions: (
|
|
207
|
-
removeTag: (state, tagToRemove: string) => ({
|
|
353
|
+
removeDetails: (ctx) => ({ details: Symbol.for("delete") }),
|
|
354
|
+
removeDimensions: (ctx) => ({ details: { dimensions: Symbol.for("delete") } }),
|
|
355
|
+
removeTag: ({state}, tagToRemove: string) => ({
|
|
208
356
|
tags: state.tags.filter(tag => tag !== tagToRemove)
|
|
209
357
|
}),
|
|
210
|
-
clearAllExceptId: (
|
|
358
|
+
clearAllExceptId: (ctx) => ({
|
|
211
359
|
name: Symbol.for("delete"),
|
|
212
360
|
details: Symbol.for("delete"),
|
|
213
361
|
tags: Symbol.for("delete")
|
|
@@ -243,10 +391,12 @@ runDeleteExample();
|
|
|
243
391
|
|
|
244
392
|
### Persistence
|
|
245
393
|
|
|
246
|
-
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.
|
|
247
395
|
|
|
248
396
|
```tsx
|
|
249
|
-
import { createStore
|
|
397
|
+
import { createStore } from '@asaidimu/react-store';
|
|
398
|
+
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
|
|
399
|
+
import React, { useEffect } from 'react';
|
|
250
400
|
|
|
251
401
|
// 1. Using WebStoragePersistence (localStorage by default)
|
|
252
402
|
// Data persists even if the browser tab is closed and reopened.
|
|
@@ -255,7 +405,7 @@ const useLocalStore = createStore(
|
|
|
255
405
|
{
|
|
256
406
|
state: { sessionCount: 0, lastVisited: new Date().toISOString() },
|
|
257
407
|
actions: {
|
|
258
|
-
incrementSessionCount: (state) => ({ sessionCount: state.sessionCount + 1 }),
|
|
408
|
+
incrementSessionCount: ({state}) => ({ sessionCount: state.sessionCount + 1 }),
|
|
259
409
|
updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
|
|
260
410
|
},
|
|
261
411
|
},
|
|
@@ -283,7 +433,7 @@ const useUserProfileStore = createStore(
|
|
|
283
433
|
state: { userId: '', preferences: { language: 'en', darkMode: false } },
|
|
284
434
|
actions: {
|
|
285
435
|
setUserId: (state, id: string) => ({ userId: id }),
|
|
286
|
-
toggleDarkMode: (state) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
436
|
+
toggleDarkMode: ({state}) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
287
437
|
},
|
|
288
438
|
},
|
|
289
439
|
{ persistence: indexedDBPersistence },
|
|
@@ -296,7 +446,7 @@ function AppWithPersistence() {
|
|
|
296
446
|
const sessionCount = selectLocal(s => s.sessionCount);
|
|
297
447
|
const darkMode = selectProfile(s => s.preferences.darkMode);
|
|
298
448
|
|
|
299
|
-
|
|
449
|
+
useEffect(() => {
|
|
300
450
|
if (localReady) {
|
|
301
451
|
actionsLocal.incrementSessionCount();
|
|
302
452
|
actionsLocal.updateLastVisited();
|
|
@@ -304,7 +454,7 @@ function AppWithPersistence() {
|
|
|
304
454
|
if (profileReady && !selectProfile(s => s.userId)) {
|
|
305
455
|
actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
|
|
306
456
|
}
|
|
307
|
-
}, [localReady, profileReady]);
|
|
457
|
+
}, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]); // Added dependencies for useEffect
|
|
308
458
|
|
|
309
459
|
if (!localReady || !profileReady) {
|
|
310
460
|
return <div>Loading persisted data...</div>;
|
|
@@ -325,10 +475,11 @@ function AppWithPersistence() {
|
|
|
325
475
|
|
|
326
476
|
### Middleware
|
|
327
477
|
|
|
328
|
-
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.
|
|
329
479
|
|
|
330
480
|
```typescript
|
|
331
|
-
import { createStore, Middleware, BlockingMiddleware } from '@asaidimu/react-store';
|
|
481
|
+
import { createStore, type Middleware, type BlockingMiddleware } from '@asaidimu/react-store';
|
|
482
|
+
import React from 'react';
|
|
332
483
|
|
|
333
484
|
interface CartState {
|
|
334
485
|
items: Array<{ id: string; name: string; quantity: number; price: number }>;
|
|
@@ -359,7 +510,7 @@ const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) =>
|
|
|
359
510
|
const useCartStore = createStore({
|
|
360
511
|
state: { items: [], total: 0 },
|
|
361
512
|
actions: {
|
|
362
|
-
addItem: (state, item: { id: string; name: string; price: number }) => {
|
|
513
|
+
addItem: ({state}, item: { id: string; name: string; price: number }) => {
|
|
363
514
|
const existingItem = state.items.find(i => i.id === item.id);
|
|
364
515
|
if (existingItem) {
|
|
365
516
|
return {
|
|
@@ -372,10 +523,11 @@ const useCartStore = createStore({
|
|
|
372
523
|
items: [...state.items, { ...item, quantity: 1 }],
|
|
373
524
|
};
|
|
374
525
|
},
|
|
375
|
-
updateQuantity: (state, id: string, quantity: number) => ({
|
|
526
|
+
updateQuantity: ({state}, id: string, quantity: number) => ({
|
|
376
527
|
items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
|
|
377
528
|
}),
|
|
378
529
|
},
|
|
530
|
+
// Middleware now accepts an object mapping names to middleware functions
|
|
379
531
|
middleware: { calculateTotal: calculateTotalMiddleware },
|
|
380
532
|
blockingMiddleware: { validateItem: validateItemMiddleware },
|
|
381
533
|
});
|
|
@@ -407,10 +559,11 @@ function CartComponent() {
|
|
|
407
559
|
|
|
408
560
|
### Observability
|
|
409
561
|
|
|
410
|
-
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.
|
|
411
563
|
|
|
412
564
|
```tsx
|
|
413
565
|
import { createStore } from '@asaidimu/react-store';
|
|
566
|
+
import React from 'react';
|
|
414
567
|
|
|
415
568
|
const useObservedStore = createStore(
|
|
416
569
|
{
|
|
@@ -421,7 +574,7 @@ const useObservedStore = createStore(
|
|
|
421
574
|
},
|
|
422
575
|
},
|
|
423
576
|
{
|
|
424
|
-
enableMetrics: true, // Crucial for enabling the 'observer'
|
|
577
|
+
enableMetrics: true, // Crucial for enabling the 'observer' and 'actionTracker' objects
|
|
425
578
|
enableConsoleLogging: true, // Log events directly to browser console
|
|
426
579
|
logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
|
|
427
580
|
performanceThresholds: {
|
|
@@ -440,16 +593,16 @@ function DebugPanel() {
|
|
|
440
593
|
// Access performance metrics
|
|
441
594
|
const metrics = observer?.getPerformanceMetrics();
|
|
442
595
|
|
|
443
|
-
// Access state history for time travel
|
|
596
|
+
// Access state history for time travel (if maxStateHistory > 0)
|
|
444
597
|
const timeTravel = observer?.createTimeTravel();
|
|
445
598
|
|
|
446
599
|
// Access action execution history
|
|
447
|
-
const actionHistory = actionTracker
|
|
600
|
+
const actionHistory = actionTracker?.getExecutions() || []; // actionTracker is only available if enableMetrics is true
|
|
448
601
|
|
|
449
602
|
return (
|
|
450
603
|
<div>
|
|
451
604
|
<h2>Debug Panel</h2>
|
|
452
|
-
{observer && (
|
|
605
|
+
{observer && ( // Check if observer is enabled
|
|
453
606
|
<>
|
|
454
607
|
<h3>Performance Metrics</h3>
|
|
455
608
|
<p>Update Count: {metrics?.updateCount}</p>
|
|
@@ -480,47 +633,50 @@ function DebugPanel() {
|
|
|
480
633
|
|
|
481
634
|
### Remote Observability
|
|
482
635
|
|
|
483
|
-
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.
|
|
484
637
|
|
|
485
638
|
```tsx
|
|
486
|
-
import { createStore
|
|
639
|
+
import { createStore } from '@asaidimu/react-store';
|
|
640
|
+
import { useRemoteObservability } from '@asaidimu/utils-store';
|
|
487
641
|
import React, { useEffect } from 'react';
|
|
488
642
|
|
|
489
643
|
const useRemoteStore = createStore(
|
|
490
644
|
{
|
|
491
645
|
state: { apiCallsMade: 0, lastApiError: null },
|
|
492
646
|
actions: {
|
|
493
|
-
simulateApiCall: async (state) => {
|
|
494
|
-
// Simulate an error 10% of the time
|
|
647
|
+
simulateApiCall: async ({state}) => {
|
|
495
648
|
if (Math.random() < 0.1) {
|
|
496
649
|
throw new Error('API request failed');
|
|
497
650
|
}
|
|
498
651
|
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
|
|
499
652
|
},
|
|
500
|
-
handleApiError: (state, error: string) => ({ lastApiError: error })
|
|
653
|
+
handleApiError: ({state}, error: string) => ({ lastApiError: error })
|
|
501
654
|
},
|
|
502
655
|
},
|
|
503
656
|
{
|
|
504
657
|
enableMetrics: true, // Required for RemoteObservability
|
|
505
658
|
enableConsoleLogging: false,
|
|
506
|
-
collectCategories
|
|
507
|
-
|
|
508
|
-
errors: true,
|
|
509
|
-
stateChanges: true,
|
|
510
|
-
middleware: true,
|
|
511
|
-
},
|
|
512
|
-
reportingInterval: 10000, // Send metrics every 10 seconds
|
|
513
|
-
batchSize: 10, // Send after 10 metrics or interval, whichever comes first
|
|
514
|
-
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
|
|
515
661
|
}
|
|
516
662
|
);
|
|
517
663
|
|
|
518
664
|
function MonitoringIntegration() {
|
|
519
665
|
const { store, observer } = useRemoteStore();
|
|
666
|
+
// useRemoteObservability is assumed to be part of utils-store or a separate utility
|
|
520
667
|
const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
|
|
521
668
|
serviceName: 'my-react-app',
|
|
522
669
|
environment: 'development',
|
|
523
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
|
|
524
680
|
});
|
|
525
681
|
|
|
526
682
|
useEffect(() => {
|
|
@@ -551,24 +707,31 @@ function MonitoringIntegration() {
|
|
|
551
707
|
}, 5000); // Report every 5 seconds
|
|
552
708
|
|
|
553
709
|
return () => clearInterval(interval);
|
|
554
|
-
}, []);
|
|
710
|
+
}, [observer, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination]); // Added dependencies
|
|
555
711
|
|
|
556
712
|
return null; // This component doesn't render anything visually
|
|
557
713
|
}
|
|
558
714
|
|
|
559
|
-
// In your App component:
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
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
|
+
// }
|
|
564
726
|
```
|
|
565
727
|
|
|
566
728
|
### Transaction Support
|
|
567
729
|
|
|
568
|
-
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.
|
|
569
731
|
|
|
570
732
|
```typescript
|
|
571
733
|
import { createStore } from '@asaidimu/react-store';
|
|
734
|
+
import React from 'react';
|
|
572
735
|
|
|
573
736
|
interface BankState {
|
|
574
737
|
checking: number;
|
|
@@ -579,7 +742,7 @@ interface BankState {
|
|
|
579
742
|
const useBankStore = createStore<BankState, any>({
|
|
580
743
|
state: { checking: 1000, savings: 500, transactions: [] },
|
|
581
744
|
actions: {
|
|
582
|
-
transferFunds: async (state, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
745
|
+
transferFunds: async ({state}, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
583
746
|
if (amount <= 0) {
|
|
584
747
|
throw new Error('Transfer amount must be positive.');
|
|
585
748
|
}
|
|
@@ -607,14 +770,15 @@ const useBankStore = createStore<BankState, any>({
|
|
|
607
770
|
});
|
|
608
771
|
|
|
609
772
|
function BankApp() {
|
|
610
|
-
const { select, actions } = useBankStore();
|
|
773
|
+
const { select, actions, store } = useBankStore();
|
|
611
774
|
const checkingBalance = select(s => s.checking);
|
|
612
775
|
const savingsBalance = select(s => s.savings);
|
|
613
776
|
const transactions = select(s => s.transactions);
|
|
614
777
|
|
|
615
778
|
const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
|
|
616
779
|
try {
|
|
617
|
-
|
|
780
|
+
// Wrap the action call in a store transaction
|
|
781
|
+
await store.transaction(() => actions.transferFunds(from, to, amount));
|
|
618
782
|
alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
|
|
619
783
|
} catch (error) {
|
|
620
784
|
alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -642,7 +806,7 @@ function BankApp() {
|
|
|
642
806
|
|
|
643
807
|
### Event System
|
|
644
808
|
|
|
645
|
-
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()`.
|
|
646
810
|
|
|
647
811
|
```typescript
|
|
648
812
|
import { createStore } from '@asaidimu/react-store';
|
|
@@ -652,7 +816,7 @@ const useEventStore = createStore(
|
|
|
652
816
|
{
|
|
653
817
|
state: { data: 'initial', processedCount: 0 },
|
|
654
818
|
actions: {
|
|
655
|
-
processData: (state, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
819
|
+
processData: ({state}, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
656
820
|
triggerError: () => { throw new Error("Action failed intentionally"); }
|
|
657
821
|
},
|
|
658
822
|
middleware: {
|
|
@@ -665,7 +829,7 @@ const useEventStore = createStore(
|
|
|
665
829
|
);
|
|
666
830
|
|
|
667
831
|
function EventMonitor() {
|
|
668
|
-
const { store } = useEventStore();
|
|
832
|
+
const { store, actions } = useEventStore();
|
|
669
833
|
const [eventLogs, setEventLogs] = React.useState<string[]>([]);
|
|
670
834
|
|
|
671
835
|
useEffect(() => {
|
|
@@ -718,8 +882,6 @@ function EventMonitor() {
|
|
|
718
882
|
};
|
|
719
883
|
}, [store]); // Re-subscribe if store instance changes (unlikely)
|
|
720
884
|
|
|
721
|
-
const { actions } = useEventStore();
|
|
722
|
-
|
|
723
885
|
return (
|
|
724
886
|
<div>
|
|
725
887
|
<h3>Store Event Log</h3>
|
|
@@ -756,7 +918,7 @@ const useDataStore = createStore({
|
|
|
756
918
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
757
919
|
return { items: ['Item A', 'Item B', 'Item C'] };
|
|
758
920
|
},
|
|
759
|
-
addItem: async (state, item: string) => {
|
|
921
|
+
addItem: async ({state}, item: string) => {
|
|
760
922
|
// Simulate a quick add operation
|
|
761
923
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
762
924
|
return { items: [...state.items, item] };
|
|
@@ -798,19 +960,22 @@ function DataLoader() {
|
|
|
798
960
|
The hook returned by `createStore` provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
|
|
799
961
|
|
|
800
962
|
```tsx
|
|
963
|
+
import { useStore as useMyStore } from '../ui/store'; // Assuming this is your store definition
|
|
964
|
+
|
|
801
965
|
function MyAdvancedComponent() {
|
|
802
966
|
const {
|
|
803
|
-
select, // Function to select state parts (memoized)
|
|
804
|
-
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)
|
|
805
969
|
isReady, // Boolean indicating if persistence is ready
|
|
806
|
-
store, // Direct access to the ReactiveDataStore instance
|
|
807
|
-
observer, // StoreObservability instance (if `enableMetrics` was true)
|
|
808
|
-
actionTracker, // Instance of ActionTracker for monitoring action executions
|
|
809
|
-
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)
|
|
810
974
|
watch, // Function to watch the loading status of actions
|
|
975
|
+
resolve, // Function to resolve an artifact (if artifacts are defined)
|
|
811
976
|
} = useMyStore(); // Assuming useMyStore is defined from createStore
|
|
812
977
|
|
|
813
|
-
// Example: Accessing the full state (use with caution for performance
|
|
978
|
+
// Example: Accessing the full state (use with caution for performance; `select` is preferred)
|
|
814
979
|
const fullCurrentState = state();
|
|
815
980
|
console.log("Full reactive state:", fullCurrentState);
|
|
816
981
|
|
|
@@ -821,15 +986,24 @@ function MyAdvancedComponent() {
|
|
|
821
986
|
}
|
|
822
987
|
|
|
823
988
|
// Example: Accessing action history
|
|
824
|
-
|
|
989
|
+
if (actionTracker) { // actionTracker is only available if enableMetrics is true
|
|
990
|
+
console.log("Action executions:", actionTracker.getExecutions());
|
|
991
|
+
}
|
|
825
992
|
|
|
826
993
|
// Example: Watching a specific action's loading state
|
|
827
|
-
const isLoadingSomeAction = watch('
|
|
828
|
-
console.log("Is '
|
|
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
|
+
// }
|
|
829
1002
|
|
|
830
1003
|
return (
|
|
831
1004
|
<div>
|
|
832
1005
|
{/* ... your component content ... */}
|
|
1006
|
+
<p>Store Ready: {isReady ? 'Yes' : 'No'}</p>
|
|
833
1007
|
</div>
|
|
834
1008
|
);
|
|
835
1009
|
}
|
|
@@ -837,22 +1011,19 @@ function MyAdvancedComponent() {
|
|
|
837
1011
|
|
|
838
1012
|
## Project Architecture
|
|
839
1013
|
|
|
840
|
-
`@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.
|
|
841
1015
|
|
|
842
1016
|
### Internal Structure
|
|
843
1017
|
|
|
844
|
-
The core logic of this package
|
|
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.
|
|
845
1019
|
|
|
846
1020
|
```
|
|
847
1021
|
.
|
|
848
1022
|
├── src/
|
|
849
|
-
│ ├── execution.ts #
|
|
850
|
-
│ ├──
|
|
851
|
-
│
|
|
852
|
-
|
|
853
|
-
│ ├── store.ts # Main `createStore` React hook implementation
|
|
854
|
-
│ └── types.ts # Core TypeScript types for the library
|
|
855
|
-
├── index.ts # Main entry point for the library (re-exports `createStore` and other public APIs)
|
|
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.
|
|
856
1027
|
├── package.json
|
|
857
1028
|
└── tsconfig.json
|
|
858
1029
|
```
|
|
@@ -861,37 +1032,39 @@ The core logic of this package resides directly within the `src/` directory:
|
|
|
861
1032
|
|
|
862
1033
|
This library leverages the following `@asaidimu` packages for its core functionalities:
|
|
863
1034
|
|
|
864
|
-
* **`@asaidimu/utils-store`**: Provides the foundational `ReactiveDataStore` for state management,
|
|
865
|
-
* **`@asaidimu/utils-persistence`**: Offers persistence adapters like `WebStoragePersistence` and `IndexedDBPersistence` for saving and loading state.
|
|
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.
|
|
866
1037
|
|
|
867
1038
|
### Core Components
|
|
868
1039
|
|
|
869
|
-
* **`ReactiveDataStore` (from `@asaidimu/utils-store`)**: The
|
|
870
|
-
* **`StoreObservability` (from `@asaidimu/utils-store`)**:
|
|
871
|
-
* **`
|
|
872
|
-
*
|
|
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.
|
|
873
1045
|
|
|
874
1046
|
### Data Flow
|
|
875
1047
|
|
|
876
|
-
1. **Action Dispatch**: A React component calls
|
|
877
|
-
2. **Action
|
|
878
|
-
3. **Action
|
|
879
|
-
4. **
|
|
880
|
-
5. **
|
|
881
|
-
6. **
|
|
882
|
-
7. **
|
|
883
|
-
8. **
|
|
884
|
-
9. **
|
|
885
|
-
10. **
|
|
886
|
-
11. **
|
|
887
|
-
12. **
|
|
888
|
-
13. **
|
|
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.
|
|
889
1062
|
|
|
890
1063
|
### Extension Points
|
|
891
1064
|
|
|
892
|
-
* **Custom Middleware**: Easily add your own `Middleware` or `BlockingMiddleware` functions for custom logic.
|
|
893
|
-
* **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).
|
|
894
|
-
* **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.
|
|
895
1068
|
|
|
896
1069
|
## Development & Contributing
|
|
897
1070
|
|
|
@@ -901,7 +1074,7 @@ We welcome contributions! Please follow the guidelines below.
|
|
|
901
1074
|
|
|
902
1075
|
1. **Clone the repository:**
|
|
903
1076
|
```bash
|
|
904
|
-
git clone https://github.com/asaidimu/react
|
|
1077
|
+
git clone https://github.com/asaidimu/node-react.git
|
|
905
1078
|
cd react-store
|
|
906
1079
|
```
|
|
907
1080
|
2. **Install dependencies:**
|
|
@@ -912,18 +1085,18 @@ We welcome contributions! Please follow the guidelines below.
|
|
|
912
1085
|
|
|
913
1086
|
### Scripts
|
|
914
1087
|
|
|
915
|
-
* `bun ci`: Installs dependencies
|
|
916
|
-
* `bun test`: Runs all unit tests using `Vitest
|
|
917
|
-
* `bun test:ci`: Runs tests
|
|
918
|
-
* `bun clean`: Removes the `dist` directory.
|
|
919
|
-
* `bun prebuild`:
|
|
920
|
-
* `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies
|
|
921
|
-
* `bun dev`: Starts the e-commerce dashboard demonstration
|
|
922
|
-
* `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.
|
|
923
1096
|
|
|
924
1097
|
### Testing
|
|
925
1098
|
|
|
926
|
-
Tests are written using `Vitest` and `React Testing Library
|
|
1099
|
+
Tests are written using `Vitest` and `React Testing Library` for component and hook testing.
|
|
927
1100
|
|
|
928
1101
|
To run tests:
|
|
929
1102
|
|
|
@@ -937,103 +1110,103 @@ bun test --watch
|
|
|
937
1110
|
|
|
938
1111
|
1. **Fork** the repository and create your branch from `main`.
|
|
939
1112
|
2. **Code Standards**: Ensure your code adheres to existing coding styles (TypeScript, ESLint, Prettier are configured).
|
|
940
|
-
3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all tests pass.
|
|
941
|
-
4. **Commits**: Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
|
|
942
|
-
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.
|
|
943
1116
|
|
|
944
1117
|
### Issue Reporting
|
|
945
1118
|
|
|
946
|
-
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).
|
|
947
1120
|
|
|
948
1121
|
## Additional Information
|
|
949
1122
|
|
|
950
1123
|
### Best Practices
|
|
951
1124
|
|
|
952
|
-
1. **Granular Selectors**: Always use `select((state) => state.path.to.value)` instead of `select((state) => state)` to prevent unnecessary re-renders of components.
|
|
953
|
-
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*.
|
|
954
1127
|
3. **Persistence**:
|
|
955
1128
|
* Use unique `storeId` or `storageKey` for each distinct store to avoid data conflicts.
|
|
956
|
-
* Always check the `isReady` flag for UI elements that depend on the initial state loaded from persistence.
|
|
957
|
-
4. **Middleware**: Leverage middleware for cross-cutting concerns like logging, analytics, or complex validation logic.
|
|
958
|
-
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.
|
|
959
1133
|
|
|
960
1134
|
### API Reference
|
|
961
1135
|
|
|
962
1136
|
#### `createStore(definition, options)`
|
|
963
1137
|
|
|
964
|
-
The main entry point for creating a store.
|
|
1138
|
+
The main entry point for creating a store hook.
|
|
965
1139
|
|
|
966
1140
|
```typescript
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
+
}
|
|
973
1153
|
|
|
974
|
-
interface StoreOptions<T> {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
logEvents?: { // Which event categories to log (defaults to all true if enableConsoleLogging is true)
|
|
980
|
-
updates?: boolean;
|
|
981
|
-
middleware?: boolean;
|
|
982
|
-
transactions?: boolean;
|
|
983
|
-
};
|
|
984
|
-
performanceThresholds?: { // Thresholds for logging slow operations (in ms)
|
|
985
|
-
updateTime?: number; // default: 50ms
|
|
986
|
-
middlewareTime?: number; // default: 20ms
|
|
987
|
-
};
|
|
988
|
-
persistence?: DataStorePersistence<T>; // Optional persistence adapter instance
|
|
989
|
-
debounceTime?: number; // Time in milliseconds to debounce actions (default: 0ms)
|
|
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
|
|
990
1159
|
}
|
|
991
1160
|
|
|
992
|
-
const
|
|
1161
|
+
const useStoreHook = createStore(definition, options);
|
|
993
1162
|
```
|
|
994
1163
|
|
|
995
|
-
**Returns**: A `
|
|
996
|
-
|
|
997
|
-
* `
|
|
998
|
-
* `
|
|
999
|
-
* `
|
|
1000
|
-
* `
|
|
1001
|
-
* `
|
|
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.
|
|
1002
1172
|
* `isReady`: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
|
|
1003
|
-
* `watch`: A function to watch the loading status of actions.
|
|
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.
|
|
1004
1175
|
|
|
1005
|
-
#### `ReactiveDataStore` (accessed via `
|
|
1176
|
+
#### `ReactiveDataStore` (accessed via `useStoreHook().store` from `@asaidimu/utils-store`)
|
|
1006
1177
|
|
|
1007
|
-
* `get(clone?: boolean):
|
|
1008
|
-
* `set(update: StateUpdater<
|
|
1009
|
-
* `subscribe(path: string | string[], listener: (state:
|
|
1010
|
-
* `transaction<R>(operation: () => R | Promise<R>): Promise<R
|
|
1011
|
-
* `use(middleware: Middleware<
|
|
1012
|
-
* `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.
|
|
1013
1184
|
* `removeMiddleware(id: string): boolean`: Removes a middleware by its ID.
|
|
1014
1185
|
* `isReady(): boolean`: Checks if the persistence layer has loaded its initial state.
|
|
1015
|
-
* `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`)
|
|
1016
1189
|
|
|
1017
|
-
|
|
1190
|
+
Available only if `enableMetrics` is `true` in `createStore` options.
|
|
1018
1191
|
|
|
1019
1192
|
* `getEventHistory(): DebugEvent[]`: Retrieves a history of all captured store events.
|
|
1020
|
-
* `getStateHistory():
|
|
1021
|
-
* `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.
|
|
1022
1195
|
* `getPerformanceMetrics(): StoreMetrics`: Returns an object containing performance statistics (e.g., `updateCount`, `averageUpdateTime`).
|
|
1023
1196
|
* `createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }`: Returns controls for time-travel debugging.
|
|
1024
|
-
* `createLoggingMiddleware(options?: object): Middleware<
|
|
1025
|
-
* `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.
|
|
1026
1199
|
* `clearHistory(): void`: Clears the event and state history.
|
|
1027
1200
|
* `disconnect(): void`: Cleans up all listeners and resources.
|
|
1028
1201
|
|
|
1029
|
-
#### Persistence Adapters
|
|
1202
|
+
#### Persistence Adapters (from `@asaidimu/utils-persistence`)
|
|
1030
1203
|
|
|
1031
|
-
All adapters implement `
|
|
1204
|
+
All adapters implement `SimplePersistence<T>`:
|
|
1032
1205
|
|
|
1033
1206
|
* `set(id:string, state: T): boolean | Promise<boolean>`: Persists data.
|
|
1034
|
-
* `get(): T | null | Promise<T | null>`: Retrieves data.
|
|
1035
|
-
* `subscribe(id:string, callback: (state:T) => void): () => void`: Subscribes to external changes.
|
|
1036
|
-
* `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.
|
|
1037
1210
|
|
|
1038
1211
|
##### `IndexedDBPersistence(storeId: string)`
|
|
1039
1212
|
|
|
@@ -1044,13 +1217,9 @@ All adapters implement `DataStorePersistence<T>`:
|
|
|
1044
1217
|
* **`storageKey`**: The key under which data is stored (e.g., `'app-config'`).
|
|
1045
1218
|
* **`session`**: Optional. If `true`, uses `sessionStorage`; otherwise, uses `localStorage` (default: `false`).
|
|
1046
1219
|
|
|
1047
|
-
##### `LocalStoragePersistence(storageKey: string)` (Deprecated)
|
|
1048
|
-
|
|
1049
|
-
* This is an alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
|
|
1050
|
-
|
|
1051
1220
|
### Comparison with Other State Management Solutions
|
|
1052
1221
|
|
|
1053
|
-
`@asaidimu/react-store` aims to be a comprehensive, all-in-one solution for React state management. Here's a comparison to popular alternatives:
|
|
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:
|
|
1054
1223
|
|
|
1055
1224
|
| **Feature** | **@asaidimu/react-store** | **Redux** | **Zustand** | **MobX** | **Recoil** |
|
|
1056
1225
|
| :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
|
|
@@ -1059,20 +1228,59 @@ All adapters implement `DataStorePersistence<T>`:
|
|
|
1059
1228
|
| **API Complexity** | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
|
|
1060
1229
|
| **Scalability** | High (transactions, persistence, remote metrics). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
|
|
1061
1230
|
| **Extensibility** | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
|
|
1062
|
-
| **Performance** | Optimized (selectors, reactive updates). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
|
|
1063
|
-
| **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). |
|
|
1064
1233
|
| **Persistence** | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
|
|
1065
|
-
| **Observability** | Excellent (metrics, time-travel, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
|
|
1066
|
-
| **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). |
|
|
1067
1236
|
|
|
1068
1237
|
#### Where `@asaidimu/react-store` Shines
|
|
1069
|
-
* **All-in-One**: It aims to be a single solution for state management, persistence, and observability, reducing the need for multiple external dependencies.
|
|
1070
|
-
* **Flexibility**: The robust middleware system
|
|
1071
|
-
* **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.
|
|
1072
1241
|
|
|
1073
1242
|
#### Trade-Offs
|
|
1074
|
-
* **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.
|
|
1075
|
-
* **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.
|
|
1076
1284
|
|
|
1077
1285
|
### Changelog
|
|
1078
1286
|
|
|
@@ -1085,3 +1293,4 @@ This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.m
|
|
|
1085
1293
|
### Acknowledgments
|
|
1086
1294
|
|
|
1087
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.
|