@asaidimu/react-store 1.4.11 → 1.5.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 +879 -256
- package/index.d.cts +5 -2
- package/index.d.ts +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,75 +3,183 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@asaidimu/react-store)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
A performant, type-safe state management solution for React with built-in persistence, observability, and middleware
|
|
6
|
+
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware system.
|
|
7
7
|
|
|
8
8
|
⚠️ **Beta Warning**
|
|
9
9
|
This package is currently in **beta**. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **Type-Safe**: Full TypeScript support with strict type checking.
|
|
15
|
-
- **Middleware Pipeline**: Transform or block updates with regular and blocking middleware.
|
|
16
|
-
- **Transactions**: Atomic state updates with rollback capabilities.
|
|
17
|
-
- **Persistence**: Seamless integration with IndexedDB or WebStorage (localStorage/sessionStorage), including cross-tab synchronization.
|
|
18
|
-
- **Observability**: Built-in metrics, event logging, state history, and time-travel debugging.
|
|
19
|
-
- **Remote Observability**: Send metrics to external systems like OpenTelemetry, Prometheus, or Grafana Cloud.
|
|
20
|
-
- **Performance Optimized**: Smart selector caching and efficient update propagation.
|
|
21
|
-
- **React 18+ Ready**: Built with modern React APIs for maximum compatibility.
|
|
13
|
+
### Table of Contents
|
|
22
14
|
|
|
23
|
-
|
|
15
|
+
* [Overview & Features](#overview--features)
|
|
16
|
+
* [Installation & Setup](#installation--setup)
|
|
17
|
+
* [Usage Documentation](#usage-documentation)
|
|
18
|
+
* [Creating a Store](#creating-a-store)
|
|
19
|
+
* [Using in Components](#using-in-components)
|
|
20
|
+
* [Handling Deletions](#handling-deletions)
|
|
21
|
+
* [Persistence](#persistence)
|
|
22
|
+
* [Middleware](#middleware)
|
|
23
|
+
* [Observability](#observability)
|
|
24
|
+
* [Remote Observability](#remote-observability)
|
|
25
|
+
* [Transaction Support](#transaction-support)
|
|
26
|
+
* [Event System](#event-system)
|
|
27
|
+
* [Advanced Hook Properties](#advanced-hook-properties)
|
|
28
|
+
* [Project Architecture](#project-architecture)
|
|
29
|
+
* [Development & Contributing](#development--contributing)
|
|
30
|
+
* [Additional Information](#additional-information)
|
|
31
|
+
* [Best Practices](#best-practices)
|
|
32
|
+
* [API Reference](#api-reference)
|
|
33
|
+
* [Comparison with Other State Management Solutions](#comparison-with-other-state-management-solutions)
|
|
34
|
+
* [Changelog](#changelog)
|
|
35
|
+
* [License](#license)
|
|
36
|
+
* [Acknowledgments](#acknowledgments)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Overview & Features
|
|
41
|
+
|
|
42
|
+
`@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
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
### Key Features
|
|
47
|
+
|
|
48
|
+
* 📊 **Reactive State Management**: Automatically tracks dependencies to optimize component renders and ensure efficient updates.
|
|
49
|
+
* 🛡️ **Type-Safe**: Built with TypeScript from the ground up, providing strict type checking and a safer development experience.
|
|
50
|
+
* ⚙️ **Middleware Pipeline**: Implement custom logic to transform, validate, or log state changes before they are applied. Supports both transforming and blocking middleware.
|
|
51
|
+
* 📦 **Transaction Support**: Group multiple state updates into a single atomic operation, with automatic rollback if any part of the transaction fails.
|
|
52
|
+
* 💾 **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
|
+
* 📈 **Remote Observability**: Extend monitoring by sending collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud.
|
|
55
|
+
* ⚡ **Performance Optimized**: Features intelligent selector caching and debounced actions to prevent rapid successive calls and ensure smooth application performance.
|
|
56
|
+
* ⚛️ **React 18+ Ready**: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
|
|
57
|
+
* 🗑️ **Explicit Deletions**: Use `Symbol.for("delete")` to explicitly remove properties from nested state objects.
|
|
58
|
+
|
|
59
|
+
## Installation & Setup
|
|
60
|
+
|
|
61
|
+
### Prerequisites
|
|
62
|
+
|
|
63
|
+
* Node.js (v18 or higher recommended)
|
|
64
|
+
* React (v18 or higher recommended)
|
|
65
|
+
* A package manager like `bun`, `npm`, or `yarn`.
|
|
66
|
+
|
|
67
|
+
### Installation Steps
|
|
68
|
+
|
|
69
|
+
To add `@asaidimu/react-store` to your project, run one of the following commands:
|
|
24
70
|
|
|
25
71
|
```bash
|
|
26
72
|
bun install @asaidimu/react-store
|
|
73
|
+
# or
|
|
74
|
+
npm install @asaidimu/react-store
|
|
75
|
+
# or
|
|
76
|
+
yarn add @asaidimu/react-store
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Configuration
|
|
80
|
+
|
|
81
|
+
No global configuration is required. All options are passed during store creation.
|
|
82
|
+
|
|
83
|
+
### Verification
|
|
84
|
+
|
|
85
|
+
You can verify the installation by importing `createStore` and setting up a basic store:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createStore } from '@asaidimu/react-store';
|
|
89
|
+
|
|
90
|
+
const myStore = createStore({
|
|
91
|
+
state: { value: 'hello' },
|
|
92
|
+
actions: {
|
|
93
|
+
setValue: (state, newValue: string) => ({ value: newValue }),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log(myStore().select(s => s.value)); // Should output 'hello'
|
|
27
98
|
```
|
|
28
99
|
|
|
29
|
-
|
|
100
|
+
If no errors are thrown, the package is correctly installed.
|
|
101
|
+
|
|
102
|
+
## Usage Documentation
|
|
30
103
|
|
|
31
104
|
### Creating a Store
|
|
32
105
|
|
|
106
|
+
Define your application state and actions, then create a store using `createStore`.
|
|
107
|
+
|
|
33
108
|
```tsx
|
|
109
|
+
// src/stores/myStore.ts
|
|
34
110
|
import { createStore } from '@asaidimu/react-store';
|
|
35
111
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
112
|
+
interface AppState {
|
|
113
|
+
count: number;
|
|
114
|
+
user: { name: string; loggedIn: boolean; email?: string };
|
|
115
|
+
settings: { theme: 'light' | 'dark'; notifications: boolean };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const initialState: AppState = {
|
|
119
|
+
count: 0,
|
|
120
|
+
user: { name: 'Guest', loggedIn: false },
|
|
121
|
+
settings: { theme: 'light', notifications: true },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const myStore = createStore({
|
|
125
|
+
state: initialState,
|
|
41
126
|
actions: {
|
|
42
127
|
increment: (state, amount: number) => ({ count: state.count + amount }),
|
|
43
|
-
login: async (state, username: string) => {
|
|
44
|
-
|
|
45
|
-
|
|
128
|
+
login: async (state, username: string, email: string) => {
|
|
129
|
+
// Simulate an asynchronous API call
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
131
|
+
return { user: { name: username, loggedIn: true, email } };
|
|
46
132
|
},
|
|
133
|
+
toggleTheme: (state) => ({
|
|
134
|
+
settings: { theme: state.settings.theme === 'light' ? 'dark' : 'light' },
|
|
135
|
+
}),
|
|
47
136
|
},
|
|
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
|
|
48
142
|
});
|
|
49
143
|
|
|
50
|
-
|
|
144
|
+
// Export the hook for use in components
|
|
145
|
+
export const useMyStore = myStore;
|
|
51
146
|
```
|
|
52
147
|
|
|
53
148
|
### Using in Components
|
|
54
149
|
|
|
150
|
+
Consume your store's state and actions within your React components using the exported hook.
|
|
151
|
+
|
|
55
152
|
```tsx
|
|
56
|
-
|
|
57
|
-
|
|
153
|
+
// src/components/MyComponent.tsx
|
|
154
|
+
import React from 'react';
|
|
155
|
+
import { useMyStore } from '../stores/myStore';
|
|
156
|
+
|
|
157
|
+
function MyComponent() {
|
|
158
|
+
const { select, actions, isReady } = useMyStore();
|
|
58
159
|
|
|
160
|
+
// Select specific parts of the state for granular re-renders
|
|
59
161
|
const count = select((state) => state.count);
|
|
60
162
|
const userName = select((state) => state.user.name);
|
|
163
|
+
const theme = select((state) => state.settings.theme);
|
|
61
164
|
|
|
165
|
+
// isReady indicates if persistence has loaded initial state
|
|
62
166
|
if (!isReady) {
|
|
63
|
-
return <div>Loading
|
|
167
|
+
return <div>Loading store data...</div>;
|
|
64
168
|
}
|
|
65
169
|
|
|
66
170
|
return (
|
|
67
|
-
<div>
|
|
68
|
-
<
|
|
69
|
-
<p>
|
|
70
|
-
<button onClick={() => actions.increment(1)}
|
|
71
|
-
<button onClick={() => actions.
|
|
171
|
+
<div style={{ background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333' }}>
|
|
172
|
+
<h1>Welcome, {userName}!</h1>
|
|
173
|
+
<p>Current Count: {count}</p>
|
|
174
|
+
<button onClick={() => actions.increment(1)}>Increment</button>
|
|
175
|
+
<button onClick={() => actions.increment(5)}>Increment by 5 (debounced)</button>
|
|
176
|
+
<button onClick={() => actions.login('Alice', 'alice@example.com')}>Login as Alice</button>
|
|
177
|
+
<button onClick={() => actions.toggleTheme()}>Toggle Theme</button>
|
|
72
178
|
</div>
|
|
73
179
|
);
|
|
74
180
|
}
|
|
181
|
+
|
|
182
|
+
export default MyComponent;
|
|
75
183
|
```
|
|
76
184
|
|
|
77
185
|
### Handling Deletions
|
|
@@ -79,75 +187,223 @@ function Counter() {
|
|
|
79
187
|
To remove a property from the state, use the `Symbol.for("delete")` symbol in your action’s return value. The store’s internal `merge` function will remove the specified key from the state.
|
|
80
188
|
|
|
81
189
|
#### Example
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { createStore } from '@asaidimu/react-store';
|
|
193
|
+
|
|
194
|
+
const deleteStore = createStore({
|
|
195
|
+
state: {
|
|
196
|
+
id: 'product-123',
|
|
197
|
+
name: 'Fancy Gadget',
|
|
198
|
+
details: {
|
|
199
|
+
color: 'blue',
|
|
200
|
+
weight: '1kg',
|
|
201
|
+
dimensions: { width: 10, height: 20 }
|
|
202
|
+
},
|
|
203
|
+
tags: ['electronics', 'new']
|
|
204
|
+
},
|
|
85
205
|
actions: {
|
|
86
|
-
|
|
206
|
+
removeDetails: (state) => ({ details: Symbol.for("delete") }),
|
|
207
|
+
removeDimensions: (state) => ({ details: { dimensions: Symbol.for("delete") } }),
|
|
208
|
+
removeTag: (state, tagToRemove: string) => ({
|
|
209
|
+
tags: state.tags.filter(tag => tag !== tagToRemove)
|
|
210
|
+
}),
|
|
211
|
+
clearAllExceptId: (state) => ({
|
|
212
|
+
name: Symbol.for("delete"),
|
|
213
|
+
details: Symbol.for("delete"),
|
|
214
|
+
tags: Symbol.for("delete")
|
|
215
|
+
})
|
|
87
216
|
},
|
|
88
217
|
});
|
|
89
218
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
219
|
+
async function runDeleteExample() {
|
|
220
|
+
const { select, actions } = deleteStore();
|
|
221
|
+
|
|
222
|
+
console.log("Initial state:", select(s => s));
|
|
223
|
+
// Initial state: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg', dimensions: { width: 10, height: 20 } }, tags: ['electronics', 'new'] }
|
|
224
|
+
|
|
225
|
+
await actions.removeDimensions();
|
|
226
|
+
console.log("After removing dimensions:", select(s => s));
|
|
227
|
+
// After removing dimensions: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg' }, tags: ['electronics', 'new'] }
|
|
93
228
|
|
|
94
|
-
|
|
229
|
+
await actions.removeDetails();
|
|
230
|
+
console.log("After removing details:", select(s => s));
|
|
231
|
+
// After removing details: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics', 'new'] }
|
|
232
|
+
|
|
233
|
+
await actions.removeTag('new');
|
|
234
|
+
console.log("After removing 'new' tag:", select(s => s));
|
|
235
|
+
// After removing 'new' tag: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics'] }
|
|
236
|
+
|
|
237
|
+
await actions.clearAllExceptId();
|
|
238
|
+
console.log("After clearing all except ID:", select(s => s));
|
|
239
|
+
// After clearing all except ID: { id: 'product-123' }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
runDeleteExample();
|
|
243
|
+
```
|
|
95
244
|
|
|
96
245
|
### Persistence
|
|
97
|
-
|
|
246
|
+
|
|
247
|
+
Persist your store's state across browser sessions or synchronize it across multiple tabs.
|
|
98
248
|
|
|
99
249
|
```tsx
|
|
100
250
|
import { createStore, WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/react-store';
|
|
101
251
|
|
|
102
|
-
|
|
103
|
-
|
|
252
|
+
// 1. Using WebStoragePersistence (localStorage by default)
|
|
253
|
+
// Data persists even if the browser tab is closed and reopened.
|
|
254
|
+
const localStorePersistence = new WebStoragePersistence('my-app-state-key');
|
|
255
|
+
const useLocalStore = createStore(
|
|
104
256
|
{
|
|
105
|
-
state: {
|
|
257
|
+
state: { sessionCount: 0, lastVisited: new Date().toISOString() },
|
|
106
258
|
actions: {
|
|
107
|
-
|
|
259
|
+
incrementSessionCount: (state) => ({ sessionCount: state.sessionCount + 1 }),
|
|
260
|
+
updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
|
|
108
261
|
},
|
|
109
262
|
},
|
|
110
|
-
{ persistence },
|
|
263
|
+
{ persistence: localStorePersistence },
|
|
111
264
|
);
|
|
112
265
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
266
|
+
// 2. Using WebStoragePersistence (sessionStorage)
|
|
267
|
+
// Data only persists for the duration of the browser tab. Clears on tab close.
|
|
268
|
+
const sessionStoragePersistence = new WebStoragePersistence('my-session-state-key', true);
|
|
269
|
+
const useSessionStore = createStore(
|
|
270
|
+
{
|
|
271
|
+
state: { tabSpecificData: 'initial' },
|
|
272
|
+
actions: {
|
|
273
|
+
updateTabSpecificData: (state, newData: string) => ({ tabSpecificData: newData }),
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{ persistence: sessionStoragePersistence },
|
|
277
|
+
);
|
|
119
278
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
279
|
+
// 3. Using IndexedDBPersistence
|
|
280
|
+
// Ideal for larger amounts of data, offers robust cross-tab synchronization.
|
|
281
|
+
const indexedDBPersistence = new IndexedDBPersistence('user-profile-data');
|
|
282
|
+
const useUserProfileStore = createStore(
|
|
283
|
+
{
|
|
284
|
+
state: { userId: '', preferences: { language: 'en', darkMode: false } },
|
|
285
|
+
actions: {
|
|
286
|
+
setUserId: (state, id: string) => ({ userId: id }),
|
|
287
|
+
toggleDarkMode: (state) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{ persistence: indexedDBPersistence },
|
|
291
|
+
);
|
|
123
292
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
293
|
+
function AppWithPersistence() {
|
|
294
|
+
const { select: selectLocal, actions: actionsLocal, isReady: localReady } = useLocalStore();
|
|
295
|
+
const { select: selectProfile, actions: actionsProfile, isReady: profileReady } = useUserProfileStore();
|
|
296
|
+
|
|
297
|
+
const sessionCount = selectLocal(s => s.sessionCount);
|
|
298
|
+
const darkMode = selectProfile(s => s.preferences.darkMode);
|
|
299
|
+
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
if (localReady) {
|
|
302
|
+
actionsLocal.incrementSessionCount();
|
|
303
|
+
actionsLocal.updateLastVisited();
|
|
304
|
+
}
|
|
305
|
+
if (profileReady && !selectProfile(s => s.userId)) {
|
|
306
|
+
actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
|
|
307
|
+
}
|
|
308
|
+
}, [localReady, profileReady]);
|
|
309
|
+
|
|
310
|
+
if (!localReady || !profileReady) {
|
|
311
|
+
return <div>Loading persisted data...</div>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div>
|
|
316
|
+
<h3>Local Store</h3>
|
|
317
|
+
<p>Session Count: {sessionCount}</p>
|
|
318
|
+
|
|
319
|
+
<h3>User Profile Store (IndexedDB)</h3>
|
|
320
|
+
<p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
|
|
321
|
+
<button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
127
325
|
```
|
|
128
326
|
|
|
129
327
|
### Middleware
|
|
130
328
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
329
|
+
Middleware functions can intercept and modify or block state updates. They are executed in the order they are added.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { createStore, Middleware, BlockingMiddleware } from '@asaidimu/react-store';
|
|
333
|
+
|
|
334
|
+
interface CartState {
|
|
335
|
+
items: Array<{ id: string; name: string; quantity: number; price: number }>;
|
|
336
|
+
total: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const calculateTotalMiddleware: Middleware<CartState> = (state, update) => {
|
|
340
|
+
if (update.items) {
|
|
341
|
+
const newItems = update.items as CartState['items'];
|
|
342
|
+
const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
|
|
343
|
+
return { ...update, total: newTotal };
|
|
344
|
+
}
|
|
134
345
|
return update;
|
|
135
346
|
};
|
|
136
347
|
|
|
137
|
-
const
|
|
138
|
-
if (update.
|
|
139
|
-
|
|
140
|
-
|
|
348
|
+
const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) => {
|
|
349
|
+
if (update.items) {
|
|
350
|
+
for (const item of update.items as CartState['items']) {
|
|
351
|
+
if (item.quantity < 0) {
|
|
352
|
+
console.warn('Blocked: Item quantity cannot be negative.');
|
|
353
|
+
return false; // Blocks the update
|
|
354
|
+
}
|
|
355
|
+
}
|
|
141
356
|
}
|
|
142
|
-
return true;
|
|
357
|
+
return true; // Allows the update
|
|
143
358
|
};
|
|
144
359
|
|
|
145
|
-
const
|
|
146
|
-
state: {
|
|
147
|
-
actions: {
|
|
148
|
-
|
|
149
|
-
|
|
360
|
+
const useCartStore = createStore({
|
|
361
|
+
state: { items: [], total: 0 },
|
|
362
|
+
actions: {
|
|
363
|
+
addItem: (state, item: { id: string; name: string; price: number }) => {
|
|
364
|
+
const existingItem = state.items.find(i => i.id === item.id);
|
|
365
|
+
if (existingItem) {
|
|
366
|
+
return {
|
|
367
|
+
items: state.items.map(i =>
|
|
368
|
+
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
items: [...state.items, { ...item, quantity: 1 }],
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
updateQuantity: (state, id: string, quantity: number) => ({
|
|
377
|
+
items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
|
|
378
|
+
}),
|
|
379
|
+
},
|
|
380
|
+
middleware: { calculateTotal: calculateTotalMiddleware },
|
|
381
|
+
blockingMiddleware: { validateItem: validateItemMiddleware },
|
|
150
382
|
});
|
|
383
|
+
|
|
384
|
+
function CartComponent() {
|
|
385
|
+
const { select, actions } = useCartStore();
|
|
386
|
+
const items = select(s => s.items);
|
|
387
|
+
const total = select(s => s.total);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div>
|
|
391
|
+
<h2>Shopping Cart</h2>
|
|
392
|
+
<ul>
|
|
393
|
+
{items.map(item => (
|
|
394
|
+
<li key={item.id}>
|
|
395
|
+
{item.name} ({item.quantity}) - ${item.price} each
|
|
396
|
+
<button onClick={() => actions.updateQuantity(item.id, item.quantity - 1)}>-</button>
|
|
397
|
+
<button onClick={() => actions.updateQuantity(item.id, item.quantity + 1)}>+</button>
|
|
398
|
+
</li>
|
|
399
|
+
))}
|
|
400
|
+
</ul>
|
|
401
|
+
<p>Total: ${total.toFixed(2)}</p>
|
|
402
|
+
<button onClick={() => actions.addItem({ id: 'apple', name: 'Apple', price: 1.50 })}>Add Apple</button>
|
|
403
|
+
<button onClick={() => actions.updateQuantity('apple', -1)}>Set Apple Quantity to -1 (Blocked)</button>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
151
407
|
```
|
|
152
408
|
|
|
153
409
|
### Observability
|
|
@@ -155,266 +411,633 @@ const useStore = createStore({
|
|
|
155
411
|
Enable metrics and debugging via the `store` and `observer` objects:
|
|
156
412
|
|
|
157
413
|
```tsx
|
|
158
|
-
|
|
414
|
+
import { createStore } from '@asaidimu/react-store';
|
|
415
|
+
|
|
416
|
+
const useObservedStore = createStore(
|
|
159
417
|
{
|
|
160
|
-
state: {
|
|
161
|
-
actions: {
|
|
418
|
+
state: { task: '', completed: false },
|
|
419
|
+
actions: {
|
|
420
|
+
addTask: (state, taskName: string) => ({ task: taskName, completed: false }),
|
|
421
|
+
completeTask: (state) => ({ completed: true }),
|
|
422
|
+
},
|
|
162
423
|
},
|
|
163
424
|
{
|
|
164
|
-
enableMetrics: true,
|
|
165
|
-
enableConsoleLogging: true,
|
|
166
|
-
logEvents: { updates: true, middleware:
|
|
167
|
-
|
|
425
|
+
enableMetrics: true, // Crucial for enabling the 'observer' object
|
|
426
|
+
enableConsoleLogging: true, // Log events directly to browser console
|
|
427
|
+
logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
|
|
428
|
+
performanceThresholds: {
|
|
429
|
+
updateTime: 50, // Warn if updates take longer than 50ms
|
|
430
|
+
middlewareTime: 20 // Warn if middleware takes longer than 20ms
|
|
431
|
+
},
|
|
432
|
+
maxEvents: 500, // Max number of events to keep in history
|
|
433
|
+
maxStateHistory: 50, // Max number of state snapshots for time travel
|
|
434
|
+
debounceTime: 300, // Debounce actions by 300ms to prevent rapid calls
|
|
168
435
|
},
|
|
169
436
|
);
|
|
170
437
|
|
|
171
438
|
function DebugPanel() {
|
|
172
|
-
const { observer } =
|
|
173
|
-
|
|
174
|
-
|
|
439
|
+
const { actions, observer, actionTracker } = useObservedStore();
|
|
440
|
+
|
|
441
|
+
// Access performance metrics
|
|
442
|
+
const metrics = observer?.getPerformanceMetrics();
|
|
443
|
+
|
|
444
|
+
// Access state history for time travel
|
|
445
|
+
const timeTravel = observer?.createTimeTravel();
|
|
446
|
+
|
|
447
|
+
// Access action execution history
|
|
448
|
+
const actionHistory = actionTracker.getExecutions();
|
|
175
449
|
|
|
176
450
|
return (
|
|
177
451
|
<div>
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
452
|
+
<h2>Debug Panel</h2>
|
|
453
|
+
{observer && (
|
|
454
|
+
<>
|
|
455
|
+
<h3>Performance Metrics</h3>
|
|
456
|
+
<p>Update Count: {metrics?.updateCount}</p>
|
|
457
|
+
<p>Avg Update Time: {metrics?.averageUpdateTime?.toFixed(2)}ms</p>
|
|
458
|
+
<p>Largest Update Size (paths): {metrics?.largestUpdateSize}</p>
|
|
459
|
+
|
|
460
|
+
<h3>Time Travel</h3>
|
|
461
|
+
<button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
|
|
462
|
+
<button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
|
|
463
|
+
<p>State History: {timeTravel?.getHistoryLength()}</p>
|
|
464
|
+
|
|
465
|
+
<h3>Action History</h3>
|
|
466
|
+
<ul>
|
|
467
|
+
{actionHistory.slice(0, 5).map(exec => (
|
|
468
|
+
<li key={exec.id}>
|
|
469
|
+
<strong>{exec.name}</strong> ({exec.status}) - {exec.duration.toFixed(2)}ms
|
|
470
|
+
</li>
|
|
471
|
+
))}
|
|
472
|
+
</ul>
|
|
473
|
+
</>
|
|
474
|
+
)}
|
|
475
|
+
<button onClick={() => actions.addTask('Learn React Store')}>Add Task</button>
|
|
476
|
+
<button onClick={() => actions.completeTask()}>Complete Task</button>
|
|
181
477
|
</div>
|
|
182
478
|
);
|
|
183
479
|
}
|
|
184
480
|
```
|
|
185
481
|
|
|
186
|
-
**Note**: The `debounceTime` option configures the debouncing applied to actions, preventing issues from rapid successive calls.
|
|
187
|
-
|
|
188
482
|
### Remote Observability
|
|
189
483
|
|
|
190
|
-
Send metrics to external systems
|
|
484
|
+
Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring.
|
|
191
485
|
|
|
192
486
|
```tsx
|
|
193
487
|
import { createStore, useRemoteObservability } from '@asaidimu/react-store';
|
|
488
|
+
import React, { useEffect } from 'react';
|
|
489
|
+
|
|
490
|
+
const useRemoteStore = createStore(
|
|
491
|
+
{
|
|
492
|
+
state: { apiCallsMade: 0, lastApiError: null },
|
|
493
|
+
actions: {
|
|
494
|
+
simulateApiCall: async (state) => {
|
|
495
|
+
// Simulate an error 10% of the time
|
|
496
|
+
if (Math.random() < 0.1) {
|
|
497
|
+
throw new Error('API request failed');
|
|
498
|
+
}
|
|
499
|
+
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
|
|
500
|
+
},
|
|
501
|
+
handleApiError: (state, error: string) => ({ lastApiError: error })
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
enableMetrics: true, // Required for RemoteObservability
|
|
506
|
+
enableConsoleLogging: false,
|
|
507
|
+
collectCategories: {
|
|
508
|
+
performance: true,
|
|
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
|
|
516
|
+
}
|
|
517
|
+
);
|
|
194
518
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
519
|
+
function MonitoringIntegration() {
|
|
520
|
+
const { store, observer } = useRemoteStore();
|
|
521
|
+
const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
|
|
522
|
+
serviceName: 'my-react-app',
|
|
523
|
+
environment: 'development',
|
|
524
|
+
instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
|
|
201
525
|
});
|
|
202
526
|
|
|
203
527
|
useEffect(() => {
|
|
528
|
+
// Add OpenTelemetry Collector as a destination
|
|
204
529
|
addOpenTelemetryDestination({
|
|
205
|
-
endpoint: '
|
|
206
|
-
apiKey: 'your-api-key',
|
|
530
|
+
endpoint: 'http://localhost:4318', // Default OpenTelemetry HTTP endpoint
|
|
531
|
+
apiKey: 'your-otel-api-key',
|
|
532
|
+
resource: { 'app.version': '1.0.0', 'host.name': 'frontend-server' }
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Add Prometheus Pushgateway as a destination
|
|
536
|
+
addPrometheusDestination({
|
|
537
|
+
pushgatewayUrl: 'http://localhost:9091', // Default Prometheus Pushgateway
|
|
538
|
+
jobName: 'react-store-metrics',
|
|
539
|
+
username: 'promuser', // Optional basic auth
|
|
540
|
+
password: 'prompassword',
|
|
207
541
|
});
|
|
542
|
+
|
|
543
|
+
// Add Grafana Cloud Loki as a destination (for logs/traces)
|
|
544
|
+
addGrafanaCloudDestination({
|
|
545
|
+
url: 'https://loki-prod-us-central1.grafana.net', // Example Loki endpoint
|
|
546
|
+
apiKey: 'your-grafana-cloud-api-key',
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Report current store metrics periodically (in addition to event-driven metrics)
|
|
550
|
+
const interval = setInterval(() => {
|
|
551
|
+
observer?.reportCurrentMetrics();
|
|
552
|
+
}, 5000); // Report every 5 seconds
|
|
553
|
+
|
|
554
|
+
return () => clearInterval(interval);
|
|
208
555
|
}, []);
|
|
209
556
|
|
|
210
|
-
return null;
|
|
557
|
+
return null; // This component doesn't render anything visually
|
|
211
558
|
}
|
|
559
|
+
|
|
560
|
+
// In your App component:
|
|
561
|
+
// <MonitoringIntegration />
|
|
562
|
+
// <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
|
|
563
|
+
// Simulate API Call
|
|
564
|
+
// </button>
|
|
212
565
|
```
|
|
213
566
|
|
|
214
|
-
|
|
567
|
+
### Transaction Support
|
|
215
568
|
|
|
216
|
-
|
|
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.
|
|
217
570
|
|
|
218
571
|
```typescript
|
|
219
|
-
|
|
220
|
-
state: T;
|
|
221
|
-
actions: R;
|
|
222
|
-
middleware?: Record<string, Middleware<T>>;
|
|
223
|
-
blockingMiddleware?: Record<string, BlockingMiddleware<T>>;
|
|
224
|
-
};
|
|
572
|
+
import { createStore } from '@asaidimu/react-store';
|
|
225
573
|
|
|
226
|
-
interface
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
maxStateHistory?: number;
|
|
231
|
-
logEvents?: { updates?: boolean; middleware?: boolean; transactions?: boolean };
|
|
232
|
-
performanceThresholds?: { updateTime?: number; middlewareTime?: number };
|
|
233
|
-
persistence?: DataStorePersistence<T>;
|
|
234
|
-
debounceTime?: number;
|
|
574
|
+
interface BankState {
|
|
575
|
+
checking: number;
|
|
576
|
+
savings: number;
|
|
577
|
+
transactions: string[];
|
|
235
578
|
}
|
|
236
579
|
|
|
237
|
-
const
|
|
580
|
+
const useBankStore = createStore<BankState, any>({
|
|
581
|
+
state: { checking: 1000, savings: 500, transactions: [] },
|
|
582
|
+
actions: {
|
|
583
|
+
transferFunds: async (state, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
|
|
584
|
+
if (amount <= 0) {
|
|
585
|
+
throw new Error('Transfer amount must be positive.');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const newChecking = fromAccount === 'checking' ? state.checking - amount : state.checking + amount;
|
|
589
|
+
const newSavings = fromAccount === 'savings' ? state.savings - amount : state.savings + amount;
|
|
590
|
+
|
|
591
|
+
if ((fromAccount === 'checking' && newChecking < 0) || (fromAccount === 'savings' && newSavings < 0)) {
|
|
592
|
+
throw new Error('Insufficient funds.');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Simulate a complex operation that might fail
|
|
596
|
+
if (amount > 700 && fromAccount === 'checking') {
|
|
597
|
+
throw new Error('Large transfers from checking require additional verification.');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const newTransactions = [...state.transactions, `Transfer ${amount} from ${fromAccount} to ${toAccount}`];
|
|
601
|
+
return {
|
|
602
|
+
checking: newChecking,
|
|
603
|
+
savings: newSavings,
|
|
604
|
+
transactions: newTransactions,
|
|
605
|
+
};
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
function BankApp() {
|
|
611
|
+
const { select, actions } = useBankStore();
|
|
612
|
+
const checkingBalance = select(s => s.checking);
|
|
613
|
+
const savingsBalance = select(s => s.savings);
|
|
614
|
+
const transactions = select(s => s.transactions);
|
|
615
|
+
|
|
616
|
+
const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
|
|
617
|
+
try {
|
|
618
|
+
await actions.transferFunds(from, to, amount);
|
|
619
|
+
alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
622
|
+
// State is automatically rolled back if an error occurs within the transaction
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
<div>
|
|
628
|
+
<h2>Bank Accounts</h2>
|
|
629
|
+
<p>Checking: ${checkingBalance.toFixed(2)}</p>
|
|
630
|
+
<p>Savings: ${savingsBalance.toFixed(2)}</p>
|
|
631
|
+
<h3>Recent Transactions</h3>
|
|
632
|
+
<ul>
|
|
633
|
+
{transactions.map((t, i) => <li key={i}>{t}</li>)}
|
|
634
|
+
</ul>
|
|
635
|
+
<button onClick={() => handleTransfer('checking', 'savings', 100)}>Transfer $100 (Checking to Savings)</button>
|
|
636
|
+
<button onClick={() => handleTransfer('savings', 'checking', 200)}>Transfer $200 (Savings to Checking)</button>
|
|
637
|
+
<button onClick={() => handleTransfer('checking', 'savings', 800)}>Transfer $800 (Will Fail)</button>
|
|
638
|
+
<button onClick={() => handleTransfer('checking', 'savings', 1500)}>Transfer $1500 (Insufficient Funds)</button>
|
|
639
|
+
</div>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Event System
|
|
645
|
+
|
|
646
|
+
The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects.
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { createStore } from '@asaidimu/react-store';
|
|
650
|
+
import React, { useEffect } from 'react';
|
|
651
|
+
|
|
652
|
+
const useEventStore = createStore(
|
|
653
|
+
{
|
|
654
|
+
state: { data: 'initial', processedCount: 0 },
|
|
655
|
+
actions: {
|
|
656
|
+
processData: (state, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
|
|
657
|
+
triggerError: () => { throw new Error("Action failed intentionally"); }
|
|
658
|
+
},
|
|
659
|
+
middleware: {
|
|
660
|
+
myLoggingMiddleware: (state, update) => {
|
|
661
|
+
console.log('Middleware processing:', update);
|
|
662
|
+
return update;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
function EventMonitor() {
|
|
669
|
+
const { store } = useEventStore();
|
|
670
|
+
const [eventLogs, setEventLogs] = React.useState<string[]>([]);
|
|
671
|
+
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
const addLog = (message: string) => {
|
|
674
|
+
setEventLogs(prev => [`${new Date().toLocaleTimeString()}: ${message}`, ...prev].slice(0, 10));
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// Subscribe to specific store events
|
|
678
|
+
const unsubscribeUpdateStart = store.onStoreEvent('update:start', (data) => {
|
|
679
|
+
addLog(`Update Started (timestamp: ${data.timestamp})`);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const unsubscribeUpdateComplete = store.onStoreEvent('update:complete', (data) => {
|
|
683
|
+
if (data.blocked) {
|
|
684
|
+
addLog(`Update BLOCKED by middleware or error. Error: ${data.error?.message || 'unknown'}`);
|
|
685
|
+
} else {
|
|
686
|
+
addLog(`Update Completed in ${data.duration?.toFixed(2)}ms. Paths changed: ${data.changedPaths?.join(', ')}`);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const unsubscribeMiddlewareStart = store.onStoreEvent('middleware:start', (data) => {
|
|
691
|
+
addLog(`Middleware '${data.name}' started (${data.type})`);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const unsubscribeMiddlewareError = store.onStoreEvent('middleware:error', (data) => {
|
|
695
|
+
addLog(`Middleware '${data.name}' encountered an error: ${data.error.message}`);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const unsubscribeTransactionStart = store.onStoreEvent('transaction:start', () => {
|
|
699
|
+
addLog(`Transaction Started`);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const unsubscribeTransactionError = store.onStoreEvent('transaction:error', (data) => {
|
|
703
|
+
addLog(`Transaction Failed: ${data.error.message}`);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const unsubscribePersistenceReady = store.onStoreEvent('persistence:ready', () => {
|
|
707
|
+
addLog(`Persistence is READY.`);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Cleanup subscriptions on component unmount
|
|
711
|
+
return () => {
|
|
712
|
+
unsubscribeUpdateStart();
|
|
713
|
+
unsubscribeUpdateComplete();
|
|
714
|
+
unsubscribeMiddlewareStart();
|
|
715
|
+
unsubscribeMiddlewareError();
|
|
716
|
+
unsubscribeTransactionStart();
|
|
717
|
+
unsubscribeTransactionError();
|
|
718
|
+
unsubscribePersistenceReady();
|
|
719
|
+
};
|
|
720
|
+
}, [store]); // Re-subscribe if store instance changes (unlikely)
|
|
721
|
+
|
|
722
|
+
const { actions } = useEventStore();
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<div>
|
|
726
|
+
<h3>Store Event Log</h3>
|
|
727
|
+
<button onClick={() => actions.processData('new data')}>Process Data</button>
|
|
728
|
+
<button onClick={() => actions.triggerError().catch(() => {})}>Trigger Action Error</button>
|
|
729
|
+
<button onClick={() => store.transaction(() => { actions.processData('transaction data'); throw new Error('Transaction error'); }).catch(() => {})}>
|
|
730
|
+
Simulate Transaction Error
|
|
731
|
+
</button>
|
|
732
|
+
<ul style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
|
|
733
|
+
{eventLogs.map((log, index) => <li key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log}</li>)}
|
|
734
|
+
</ul>
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
737
|
+
}
|
|
238
738
|
```
|
|
239
739
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- `select`: Memoized selector for state slices.
|
|
244
|
-
- `actions`: Action dispatchers.
|
|
245
|
-
- `actionTracker`: Tracks action executions.
|
|
246
|
-
- `state`: Getter for full state.
|
|
247
|
-
- `isReady`: Boolean indicating persistence initialization completion.
|
|
248
|
-
|
|
249
|
-
### `ReactiveDataStore` (via `store`)
|
|
250
|
-
|
|
251
|
-
- `get(): T`
|
|
252
|
-
- `set(update: StateUpdater<T>): Promise<void>`
|
|
253
|
-
- `subscribe(path: string | string[], listener: (state: T) => void): () => void`
|
|
254
|
-
- `transaction<R>(operation: () => R | Promise<R>): Promise<R>`
|
|
255
|
-
- `use(middleware: Middleware<T>, name?: string)`
|
|
256
|
-
- `useBlockingMiddleware(middleware: BlockingMiddleware<T>, name?: string)`
|
|
257
|
-
- `isReady(): boolean`
|
|
258
|
-
- `onStoreEvent(event: StoreEvent, listener): () => void`
|
|
259
|
-
|
|
260
|
-
### `StoreObservability` (via `observer`)
|
|
261
|
-
|
|
262
|
-
- `getEventHistory(): DebugEvent[]`
|
|
263
|
-
- `getStateHistory(): T[]`
|
|
264
|
-
- `getRecentChanges(limit?: number): { timestamp, changedPaths, from, to }[]`
|
|
265
|
-
- `getPerformanceMetrics(): StoreMetrics`
|
|
266
|
-
- `createTimeTravel(): { undo, redo, canUndo, canRedo }`
|
|
267
|
-
- `createLoggingMiddleware(options)`
|
|
268
|
-
- `createValidationMiddleware(validator)`
|
|
269
|
-
|
|
270
|
-
### `RemoteObservability`
|
|
271
|
-
|
|
272
|
-
- Extend `StoreObservability` with `useRemoteObservability(store, options)`:
|
|
273
|
-
- `addDestination(destination: RemoteDestination)`
|
|
274
|
-
- `beginTrace(name: string)`
|
|
275
|
-
- `beginSpan(traceId, name, labels)`
|
|
276
|
-
- `endSpan(traceId, spanName)`
|
|
277
|
-
- `endTrace(traceId)`
|
|
278
|
-
|
|
279
|
-
### Persistence Adapters
|
|
280
|
-
|
|
281
|
-
- **`IndexedDBPersistence(storeId: string)`**
|
|
282
|
-
- **`WebStoragePersistence(storageKey: string, session?: boolean)`**
|
|
283
|
-
- Methods: `set(id, state)`, `get()`, `subscribe(id, callback)`, `clear()`
|
|
284
|
-
- **`LocalStoragePersistence(storageKey: string)`** (Deprecated)
|
|
285
|
-
- Alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
|
|
286
|
-
|
|
287
|
-
## Advanced Hook Properties
|
|
288
|
-
|
|
289
|
-
The hook returned by `createStore` (e.g., `useCounter()` in examples) provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
|
|
740
|
+
### Advanced Hook Properties
|
|
741
|
+
|
|
742
|
+
The hook returned by `createStore` provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
|
|
290
743
|
|
|
291
744
|
```tsx
|
|
292
|
-
function
|
|
745
|
+
function MyAdvancedComponent() {
|
|
293
746
|
const {
|
|
294
|
-
select, // Function to select state parts
|
|
295
|
-
actions, // Object containing your defined actions
|
|
747
|
+
select, // Function to select state parts (memoized)
|
|
748
|
+
actions, // Object containing your defined actions (debounced)
|
|
296
749
|
isReady, // Boolean indicating if persistence is ready
|
|
297
750
|
store, // Direct access to the ReactiveDataStore instance
|
|
298
751
|
observer, // StoreObservability instance (if `enableMetrics` was true)
|
|
299
752
|
actionTracker, // Instance of ActionTracker for monitoring action executions
|
|
300
753
|
state, // A hook `() => T` to get the entire reactive state object
|
|
301
|
-
} =
|
|
754
|
+
} = useMyStore(); // Assuming useMyStore is defined from createStore
|
|
302
755
|
|
|
303
|
-
// Example: Accessing the full state (use with caution for performance)
|
|
756
|
+
// Example: Accessing the full state (use with caution for performance, `select` is preferred)
|
|
304
757
|
const fullCurrentState = state();
|
|
758
|
+
console.log("Full reactive state:", fullCurrentState);
|
|
759
|
+
|
|
760
|
+
// Example: Accessing observer methods (if enabled)
|
|
761
|
+
if (observer) {
|
|
762
|
+
console.log("Performance metrics:", observer.getPerformanceMetrics());
|
|
763
|
+
console.log("Recent state changes:", observer.getRecentChanges(3));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Example: Accessing action history
|
|
767
|
+
console.log("Action executions:", actionTracker.getExecutions());
|
|
305
768
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
769
|
+
return (
|
|
770
|
+
<div>
|
|
771
|
+
{/* ... your component content ... */}
|
|
772
|
+
</div>
|
|
773
|
+
);
|
|
309
774
|
}
|
|
310
775
|
```
|
|
311
776
|
|
|
312
|
-
|
|
313
|
-
- `observer`: If `enableMetrics` was true during store creation, this is the `StoreObservability` instance. It offers methods like `getPerformanceMetrics()`, `getStateHistory()`, and `createTimeTravel()` for time-travel debugging.
|
|
314
|
-
- `actionTracker`: An `ActionTracker` instance that records information about dispatched actions, including their parameters, status (success/error), and duration. Its history can be valuable for debugging action flows.
|
|
315
|
-
- `state`: A hook that, when called (e.g., `state()`), returns the entire current state object. Be mindful that using this will cause your component to re-render on *any* change to the store, unlike the more granular `select` hook. Prefer `select` for optimal component performance.
|
|
316
|
-
|
|
317
|
-
## Best Practices
|
|
318
|
-
|
|
319
|
-
1. **Granular Selectors**:
|
|
320
|
-
```tsx
|
|
321
|
-
const count = select((state) => state.count); // Good
|
|
322
|
-
const allState = select((state) => state); // Avoid
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
2. **Action Design**:
|
|
326
|
-
```tsx
|
|
327
|
-
actions: {
|
|
328
|
-
fetchData: async (state, id: string) => {
|
|
329
|
-
const data = await fetch(`/api/${id}`).then((res) => res.json());
|
|
330
|
-
return { data };
|
|
331
|
-
},
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
3. **Persistence**:
|
|
336
|
-
- Use unique `storeId` or `storageKey` to avoid conflicts.
|
|
337
|
-
- Check `isReady` for UI dependent on persisted state:
|
|
338
|
-
```tsx
|
|
339
|
-
if (!isReady) return <div>Loading...</div>;
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
4. **Middleware**:
|
|
343
|
-
```tsx
|
|
344
|
-
const apiMonitor = (state, update) => {
|
|
345
|
-
if (update.apiCalls) telemetry.log(update.apiCalls);
|
|
346
|
-
return update;
|
|
347
|
-
};
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
## Example with Persistence and Observability
|
|
777
|
+
## Project Architecture
|
|
351
778
|
|
|
352
|
-
|
|
353
|
-
import { createStore, WebStoragePersistence } from '@asaidimu/react-store';
|
|
779
|
+
`@asaidimu/react-store` is structured to provide a modular yet integrated state management solution.
|
|
354
780
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
781
|
+
```
|
|
782
|
+
.
|
|
783
|
+
├── src/
|
|
784
|
+
│ ├── hooks/
|
|
785
|
+
│ │ └── observability.ts # React hook for remote observability
|
|
786
|
+
│ ├── persistence/
|
|
787
|
+
│ │ ├── indexedb.ts # IndexedDB persistence adapter
|
|
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
|
|
807
|
+
├── package.json
|
|
808
|
+
└── tsconfig.json
|
|
809
|
+
```
|
|
375
810
|
|
|
376
|
-
|
|
377
|
-
const { select, actions, isReady, observer } = useCounter();
|
|
378
|
-
const count = select((state) => state.count);
|
|
811
|
+
### Core Components
|
|
379
812
|
|
|
380
|
-
|
|
813
|
+
* **`ReactiveDataStore` (`src/state/store.ts`)**: The heart of the library. It manages the immutable state, processes updates (including debouncing), handles middleware, transactions, and interacts with persistence adapters. It also emits detailed internal events for observability.
|
|
814
|
+
* **`StoreObservability` (`src/state/observability.ts`)**: An extension built on top of `ReactiveDataStore`'s event system. It provides debugging features like event history, state snapshots for time-travel, performance metrics, and utilities to create logging/validation middleware.
|
|
815
|
+
* **`createStore` Hook (`src/store/index.ts`)**: The primary React-facing API. It instantiates `ReactiveDataStore` and `StoreObservability`, wraps actions with debouncing and tracking, and provides the `select` hook powered by `useSyncExternalStore` for efficient component updates.
|
|
816
|
+
* **Persistence Adapters (`src/persistence/`)**: Implement the `DataStorePersistence` interface. `WebStoragePersistence` (for localStorage/sessionStorage) and `IndexedDBPersistence` provide concrete storage solutions with cross-tab synchronization.
|
|
817
|
+
* **`RemoteObservability` (`src/utils/remote-observability.ts`)**: Extends `StoreObservability` to enable sending metrics, logs, and traces to external monitoring systems. It defines a pluggable `RemoteDestination` interface and provides out-of-the-box implementations.
|
|
381
818
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
819
|
+
### Data Flow
|
|
820
|
+
|
|
821
|
+
1. **Action Dispatch**: A React component calls an action (e.g., `actions.increment(1)`). Actions are debounced by default.
|
|
822
|
+
2. **Action Execution Tracking**: The `ActionTracker` records the action's details (name, params, start time).
|
|
823
|
+
3. **State Update Request**: The action, after potential debouncing, initiates a `store.set()` call with a partial state update or a function.
|
|
824
|
+
4. **Transaction Context**: If within a `store.transaction()`, the state is snapshotted for potential rollback.
|
|
825
|
+
5. **Blocking Middleware**: Updates first pass through `blockingMiddleware`. If any middleware returns `false` or throws, the update is halted, and the state is not modified.
|
|
826
|
+
6. **Transform Middleware**: If not blocked, updates then pass through transforming `middleware`. These functions can modify the partial update.
|
|
827
|
+
7. **State Merging**: The final transformed update is immutably merged into the current state using the `merge` utility. `Symbol.for("delete")` is handled here for property removal.
|
|
828
|
+
8. **Change Detection**: The `diff` utility identifies which paths in the state have truly changed.
|
|
829
|
+
9. **Persistence**: If changes occurred, the new state is saved via the configured `DataStorePersistence` adapter (e.g., `localStorage`, `IndexedDB`). External changes from persistence are also subscribed to and applied.
|
|
830
|
+
10. **Listener Notification**: Only `React.useSyncExternalStore` subscribers whose selected paths have changed are notified, triggering re-renders of relevant components.
|
|
831
|
+
11. **Observability Events**: Throughout this flow, the `ReactiveDataStore` emits fine-grained events (`update:start`, `middleware:complete`, `transaction:error`, etc.) that `StoreObservability` captures for debugging, metrics, and remote reporting.
|
|
832
|
+
|
|
833
|
+
### Extension Points
|
|
834
|
+
|
|
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.
|
|
838
|
+
|
|
839
|
+
## Development & Contributing
|
|
840
|
+
|
|
841
|
+
We welcome contributions! Please follow the guidelines below.
|
|
842
|
+
|
|
843
|
+
### Development Setup
|
|
844
|
+
|
|
845
|
+
1. **Clone the repository:**
|
|
846
|
+
```bash
|
|
847
|
+
git clone https://github.com/asaidimu/react-store.git
|
|
848
|
+
cd react-store
|
|
849
|
+
```
|
|
850
|
+
2. **Install dependencies:**
|
|
851
|
+
This project uses `bun` as the package manager.
|
|
852
|
+
```bash
|
|
853
|
+
bun install
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Scripts
|
|
857
|
+
|
|
858
|
+
* `bun ci`: Installs dependencies (for CI/CD environments).
|
|
859
|
+
* `bun test`: Runs all unit tests using `Vitest`.
|
|
860
|
+
* `bun test:ci`: Runs tests in CI mode (single run).
|
|
861
|
+
* `bun clean`: Removes the `dist` directory.
|
|
862
|
+
* `bun prebuild`: Cleans `dist` and runs a sync script (internal).
|
|
863
|
+
* `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies.
|
|
864
|
+
* `bun dev`: Starts a development server (likely for a UI example).
|
|
865
|
+
* `bun postbuild`: Copies `README.md`, `LICENSE.md`, and `dist.package.json` into the `dist` folder.
|
|
866
|
+
|
|
867
|
+
### Testing
|
|
868
|
+
|
|
869
|
+
Tests are written using `Vitest` and `React Testing Library`.
|
|
870
|
+
|
|
871
|
+
To run tests:
|
|
872
|
+
|
|
873
|
+
```bash
|
|
874
|
+
bun test
|
|
875
|
+
# or to run in watch mode
|
|
876
|
+
bun test --watch
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### Contributing Guidelines
|
|
880
|
+
|
|
881
|
+
1. **Fork** the repository and create your branch from `main`.
|
|
882
|
+
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.
|
|
886
|
+
|
|
887
|
+
### Issue Reporting
|
|
888
|
+
|
|
889
|
+
For bugs, feature requests, or questions, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/react-store/issues).
|
|
890
|
+
|
|
891
|
+
## Additional Information
|
|
892
|
+
|
|
893
|
+
### Best Practices
|
|
894
|
+
|
|
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.
|
|
897
|
+
3. **Persistence**:
|
|
898
|
+
* 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.
|
|
902
|
+
|
|
903
|
+
### API Reference
|
|
904
|
+
|
|
905
|
+
#### `createStore(definition, options)`
|
|
906
|
+
|
|
907
|
+
The main entry point for creating a store.
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
type StoreDefinition<T, R extends Actions<T>> = {
|
|
911
|
+
state: T; // Initial state object
|
|
912
|
+
actions: R; // Object mapping action names to action functions
|
|
913
|
+
middleware?: Record<string, Middleware<T>>; // Optional transforming middleware
|
|
914
|
+
blockingMiddleware?: Record<string, BlockingMiddleware<T>>; // Optional blocking middleware
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
interface StoreOptions<T> {
|
|
918
|
+
enableMetrics?: boolean; // Enable StoreObservability features (default: false)
|
|
919
|
+
enableConsoleLogging?: boolean; // Log store events to console (default: false)
|
|
920
|
+
maxEvents?: number; // Maximum number of events to keep in history (default: 500)
|
|
921
|
+
maxStateHistory?: number; // Maximum number of state snapshots for time travel (default: 20)
|
|
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)
|
|
389
933
|
}
|
|
934
|
+
|
|
935
|
+
const useStore = createStore(definition, options);
|
|
390
936
|
```
|
|
391
937
|
|
|
392
|
-
|
|
938
|
+
**Returns**: A `useStore` hook which, when called in a component, returns an object with:
|
|
939
|
+
* `store`: Direct access to the `ReactiveDataStore` instance.
|
|
940
|
+
* `observer`: The `StoreObservability` instance (available if `enableMetrics` is `true`). Provides debug and monitoring utilities.
|
|
941
|
+
* `select`: A memoized selector function to extract specific state slices. Re-renders components only when selected data changes.
|
|
942
|
+
* `actions`: An object containing your defined actions. These actions are debounced and tracked.
|
|
943
|
+
* `actionTracker`: An instance of `ActionTracker` for monitoring the execution history of your actions.
|
|
944
|
+
* `state`: A hook `() => T` that returns the entire current state object. Use sparingly as it will cause re-renders on *any* state change.
|
|
945
|
+
* `isReady`: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
|
|
946
|
+
|
|
947
|
+
#### `ReactiveDataStore` (accessed via `useStore().store`)
|
|
948
|
+
|
|
949
|
+
* `get(clone?: boolean): T`: Retrieves the current state. Pass `true` to get a deep clone (recommended for mutations outside of actions).
|
|
950
|
+
* `set(update: StateUpdater<T>): Promise<void>`: Updates the state with a partial object or a function returning a partial object.
|
|
951
|
+
* `subscribe(path: string | string[], listener: (state: T) => void): () => void`: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.
|
|
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<T>, name?: string): string`: Adds a transforming middleware. Returns its ID.
|
|
954
|
+
* `useBlockingMiddleware(middleware: BlockingMiddleware<T>, name?: string): string`: Adds a blocking middleware. Returns its ID.
|
|
955
|
+
* `removeMiddleware(id: string): boolean`: Removes a middleware by its ID.
|
|
956
|
+
* `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'`).
|
|
958
|
+
|
|
959
|
+
#### `StoreObservability` (accessed via `useStore().observer`)
|
|
960
|
+
|
|
961
|
+
* `getEventHistory(): DebugEvent[]`: Retrieves a history of all captured store events.
|
|
962
|
+
* `getStateHistory(): T[]`: Returns a history of state snapshots, enabling time-travel.
|
|
963
|
+
* `getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: Partial<T>; to: Partial<T>; }>`: Provides a simplified view of recent state changes.
|
|
964
|
+
* `getPerformanceMetrics(): StoreMetrics`: Returns an object containing performance statistics (e.g., `updateCount`, `averageUpdateTime`).
|
|
965
|
+
* `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<T>`: A factory for a simple logging middleware.
|
|
967
|
+
* `createValidationMiddleware(validator: (state: T, update: DeepPartial<T>) => boolean | { valid: boolean; reason?: string }): BlockingMiddleware<T>`: A factory for a schema validation middleware.
|
|
968
|
+
* `clearHistory(): void`: Clears the event and state history.
|
|
969
|
+
* `disconnect(): void`: Cleans up all listeners and resources.
|
|
970
|
+
|
|
971
|
+
#### `RemoteObservability` (accessed via `useRemoteObservability` hook)
|
|
972
|
+
|
|
973
|
+
Extends `StoreObservability` with methods for sending metrics and traces externally.
|
|
393
974
|
|
|
394
|
-
|
|
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.
|
|
984
|
+
|
|
985
|
+
#### Persistence Adapters
|
|
986
|
+
|
|
987
|
+
All adapters implement `DataStorePersistence<T>`:
|
|
988
|
+
|
|
989
|
+
* `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.
|
|
993
|
+
|
|
994
|
+
##### `IndexedDBPersistence(storeId: string)`
|
|
995
|
+
|
|
996
|
+
* **`storeId`**: A unique identifier for the IndexedDB object store (e.g., `'user-data'`).
|
|
997
|
+
|
|
998
|
+
##### `WebStoragePersistence(storageKey: string, session?: boolean)`
|
|
999
|
+
|
|
1000
|
+
* **`storageKey`**: The key under which data is stored (e.g., `'app-config'`).
|
|
1001
|
+
* **`session`**: Optional. If `true`, uses `sessionStorage`; otherwise, uses `localStorage` (default: `false`).
|
|
1002
|
+
|
|
1003
|
+
##### `LocalStoragePersistence(storageKey: string)` (Deprecated)
|
|
1004
|
+
|
|
1005
|
+
* This is an alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
|
|
1006
|
+
|
|
1007
|
+
### Comparison with Other State Management Solutions
|
|
1008
|
+
|
|
1009
|
+
`@asaidimu/react-store` aims to provide a comprehensive, all-in-one solution for React state management. Here's a comparison to popular alternatives:
|
|
395
1010
|
|
|
396
1011
|
| **Feature** | **@asaidimu/react-store** | **Redux** | **Zustand** | **MobX** | **Recoil** |
|
|
397
|
-
|
|
1012
|
+
| :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
|
|
398
1013
|
| **Dev Experience** | Intuitive hook-based API with rich tooling. | Verbose setup with reducers and middleware. | Minimalist, hook-friendly API. | Reactive, class-based approach. | Atom-based, React-native feel. |
|
|
399
1014
|
| **Learning Curve** | Moderate (middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). |
|
|
400
1015
|
| **API Complexity** | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
|
|
401
1016
|
| **Scalability** | High (transactions, persistence, remote metrics). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
|
|
402
1017
|
| **Extensibility** | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
|
|
403
1018
|
| **Performance** | Optimized (selectors, reactive updates). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
|
|
404
|
-
| **Bundle Size** | Moderate (includes observability, persistence). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
|
|
1019
|
+
| **Bundle Size** | Moderate (includes observability, persistence, remote observability). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
|
|
405
1020
|
| **Persistence** | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
|
|
406
1021
|
| **Observability** | Excellent (metrics, time-travel, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
|
|
407
|
-
| **React Integration** | Native (hooks) | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
|
|
1022
|
+
| **React Integration** | Native (hooks, `useSyncExternalStore`) | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
|
|
1023
|
+
|
|
1024
|
+
#### 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 and transaction support make it highly adaptable to complex business logic and data flows.
|
|
1027
|
+
* **Modern React**: It leverages `useSyncExternalStore` for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders.
|
|
1028
|
+
|
|
1029
|
+
#### 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 concepts, though the API strives for simplicity.
|
|
1032
|
+
|
|
1033
|
+
### Changelog
|
|
1034
|
+
|
|
1035
|
+
For a detailed history of changes and new features, please refer to the [CHANGELOG.md](./CHANGELOG.md) file.
|
|
408
1036
|
|
|
409
|
-
###
|
|
410
|
-
- **All-in-One**: Combines state management, persistence, and observability without external dependencies.
|
|
411
|
-
- **Flexibility**: Middleware and transactions make it adaptable to complex use cases.
|
|
412
|
-
- **Modern React**: Leverages `useSyncExternalStore` for optimal performance and compatibility.
|
|
1037
|
+
### License
|
|
413
1038
|
|
|
414
|
-
|
|
415
|
-
- Larger bundle size compared to minimalist solutions like Zustand.
|
|
416
|
-
- Slightly steeper learning curve due to advanced features.
|
|
1039
|
+
This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.md) file for full details.
|
|
417
1040
|
|
|
418
|
-
|
|
1041
|
+
### Acknowledgments
|
|
419
1042
|
|
|
420
|
-
|
|
1043
|
+
Developed by [Saidimu](https://github.com/asaidimu).
|