dev-react-microstore 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -2
- package/dist/index.d.mts +85 -2
- package/dist/index.d.ts +85 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/example/README.md +54 -0
- package/example/eslint.config.js +28 -0
- package/example/index.html +13 -0
- package/example/package-lock.json +3382 -0
- package/example/package.json +29 -0
- package/example/public/index.html +98 -0
- package/example/public/vite.svg +1 -0
- package/example/src/App.css +613 -0
- package/example/src/App.tsx +34 -0
- package/example/src/assets/react.svg +1 -0
- package/example/src/components/Counter.tsx +112 -0
- package/example/src/components/CustomCompare.tsx +466 -0
- package/example/src/components/Logs.tsx +28 -0
- package/example/src/components/Search.tsx +38 -0
- package/example/src/components/ThemeToggle.tsx +25 -0
- package/example/src/components/TodoList.tsx +63 -0
- package/example/src/components/UserManager.tsx +68 -0
- package/example/src/index.css +68 -0
- package/example/src/main.tsx +10 -0
- package/example/src/store.ts +223 -0
- package/example/src/vite-env.d.ts +1 -0
- package/example/tsconfig.app.json +26 -0
- package/example/tsconfig.json +7 -0
- package/example/tsconfig.node.json +25 -0
- package/example/vite.config.ts +7 -0
- package/package.json +10 -3
- package/src/index.ts +247 -12
package/src/index.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { useMemo, useRef, useSyncExternalStore } from 'react'
|
|
|
2
2
|
|
|
3
3
|
type StoreListener = () => void;
|
|
4
4
|
|
|
5
|
+
type MiddlewareFunction<T extends object> = (
|
|
6
|
+
currentState: T,
|
|
7
|
+
update: Partial<T>,
|
|
8
|
+
next: (modifiedUpdate?: Partial<T>) => void
|
|
9
|
+
) => void;
|
|
10
|
+
|
|
5
11
|
function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
|
|
6
12
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
7
13
|
return ((...args: Parameters<T>) => {
|
|
@@ -10,23 +16,95 @@ function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
|
|
|
10
16
|
}) as T;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new reactive store with fine-grained subscriptions and middleware support.
|
|
21
|
+
*
|
|
22
|
+
* @param initialState - The initial state object for the store
|
|
23
|
+
* @returns Store object with methods: get, set, subscribe, select, addMiddleware
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const store = createStoreState({ count: 0, name: 'John' });
|
|
28
|
+
* store.set({ count: 1 }); // Update state
|
|
29
|
+
* const { count } = useStoreSelector(store, ['count']); // Subscribe in React
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
13
32
|
export function createStoreState<T extends object>(
|
|
14
|
-
initialState: T
|
|
33
|
+
initialState: T
|
|
15
34
|
) {
|
|
16
35
|
let state = initialState;
|
|
17
36
|
const keyListeners = new Map<keyof T, Set<StoreListener>>();
|
|
18
37
|
const debouncedNotifiers = new Map<keyof T, () => void>();
|
|
38
|
+
|
|
39
|
+
// Middleware storage
|
|
40
|
+
const middleware: Array<{ callback: MiddlewareFunction<T>; keys: (keyof T)[] | null }> = [];
|
|
41
|
+
|
|
42
|
+
|
|
19
43
|
|
|
20
44
|
const get = () => state;
|
|
21
45
|
|
|
22
|
-
const set = (
|
|
23
|
-
if (!
|
|
46
|
+
const set = (update: Partial<T>, debounceDelay: number | boolean = false) => {
|
|
47
|
+
if (!update) return;
|
|
48
|
+
|
|
49
|
+
// Run middleware chain
|
|
50
|
+
let currentUpdate = update;
|
|
51
|
+
let middlewareIndex = 0;
|
|
52
|
+
let blocked = false;
|
|
53
|
+
|
|
54
|
+
const runMiddleware = (modifiedUpdate?: Partial<T>) => {
|
|
55
|
+
if (modifiedUpdate !== undefined) {
|
|
56
|
+
currentUpdate = modifiedUpdate;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (middlewareIndex >= middleware.length) {
|
|
60
|
+
// All middleware processed, apply the update
|
|
61
|
+
if (!blocked) {
|
|
62
|
+
applyUpdate(currentUpdate, debounceDelay);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentMiddleware = middleware[middlewareIndex++];
|
|
68
|
+
|
|
69
|
+
// Check if middleware applies to these keys
|
|
70
|
+
if (!currentMiddleware.keys || currentMiddleware.keys.some(key => key in currentUpdate)) {
|
|
71
|
+
let nextCalled = false;
|
|
72
|
+
|
|
73
|
+
const next = (modifiedUpdate?: Partial<T>) => {
|
|
74
|
+
if (nextCalled) return; // Prevent multiple calls
|
|
75
|
+
nextCalled = true;
|
|
76
|
+
runMiddleware(modifiedUpdate);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
currentMiddleware.callback(state, currentUpdate, next);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
blocked = true;
|
|
83
|
+
console.error('Middleware error:', error);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If next() wasn't called, the middleware blocked the update
|
|
88
|
+
if (!nextCalled) {
|
|
89
|
+
blocked = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// Skip this middleware
|
|
94
|
+
runMiddleware();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
runMiddleware();
|
|
99
|
+
};
|
|
24
100
|
|
|
101
|
+
const applyUpdate = (processedUpdate: Partial<T>, debounceDelay: number | boolean) => {
|
|
25
102
|
const updatedKeys: (keyof T)[] = [];
|
|
26
|
-
|
|
103
|
+
|
|
104
|
+
for (const key in processedUpdate) {
|
|
27
105
|
const typedKey = key as keyof T;
|
|
28
106
|
const currentValue = state[typedKey];
|
|
29
|
-
const nextValue =
|
|
107
|
+
const nextValue = processedUpdate[typedKey];
|
|
30
108
|
|
|
31
109
|
if (currentValue === nextValue) continue;
|
|
32
110
|
|
|
@@ -38,14 +116,12 @@ export function createStoreState<T extends object>(
|
|
|
38
116
|
|
|
39
117
|
if (updatedKeys.length === 0) return;
|
|
40
118
|
|
|
41
|
-
|
|
42
119
|
for (const key of updatedKeys) {
|
|
43
|
-
|
|
44
|
-
if (typeof debounceDelay === 'number') {
|
|
120
|
+
if (debounceDelay !== false) {
|
|
45
121
|
if (!debouncedNotifiers.has(key)) {
|
|
46
122
|
debouncedNotifiers.set(key, debounce(() => {
|
|
47
123
|
keyListeners.get(key)?.forEach(listener => listener());
|
|
48
|
-
}, debounceDelay));
|
|
124
|
+
}, typeof debounceDelay === 'number' ? debounceDelay : 0));
|
|
49
125
|
}
|
|
50
126
|
debouncedNotifiers.get(key)!();
|
|
51
127
|
} else {
|
|
@@ -54,6 +130,33 @@ export function createStoreState<T extends object>(
|
|
|
54
130
|
}
|
|
55
131
|
};
|
|
56
132
|
|
|
133
|
+
const addMiddleware = (
|
|
134
|
+
callbackOrTuple: MiddlewareFunction<T> | [MiddlewareFunction<T>, (keyof T)[]],
|
|
135
|
+
affectedKeys: (keyof T)[] | null = null
|
|
136
|
+
) => {
|
|
137
|
+
let callback: MiddlewareFunction<T>;
|
|
138
|
+
let keys: (keyof T)[] | null;
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(callbackOrTuple)) {
|
|
141
|
+
[callback, keys] = callbackOrTuple;
|
|
142
|
+
} else {
|
|
143
|
+
callback = callbackOrTuple;
|
|
144
|
+
keys = affectedKeys;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const middlewareItem = { callback, keys };
|
|
148
|
+
middleware.push(middlewareItem);
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
const index = middleware.indexOf(middlewareItem);
|
|
152
|
+
if (index > -1) {
|
|
153
|
+
middleware.splice(index, 1);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
57
160
|
const subscribe = (keys: (keyof T)[], listener: StoreListener): (() => void) => {
|
|
58
161
|
for (const key of keys) {
|
|
59
162
|
if (!keyListeners.has(key)) {
|
|
@@ -78,7 +181,7 @@ export function createStoreState<T extends object>(
|
|
|
78
181
|
return result;
|
|
79
182
|
};
|
|
80
183
|
|
|
81
|
-
return { get, set, subscribe, select };
|
|
184
|
+
return { get, set, subscribe, select, addMiddleware };
|
|
82
185
|
}
|
|
83
186
|
|
|
84
187
|
type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
|
|
@@ -111,6 +214,25 @@ function shallowEqualSelector<T extends object>(
|
|
|
111
214
|
return a.length === b.length && a.every((item, i) => item === b[i]);
|
|
112
215
|
}
|
|
113
216
|
|
|
217
|
+
/**
|
|
218
|
+
* React hook that subscribes to specific keys in a store with fine-grained re-renders.
|
|
219
|
+
* Only re-renders when the selected keys actually change (using Object.is comparison).
|
|
220
|
+
*
|
|
221
|
+
* @param store - The store created with createStoreState
|
|
222
|
+
* @param selector - Array of keys to subscribe to, or objects with custom compare functions
|
|
223
|
+
* @returns Selected state values from the store
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* // Subscribe to specific keys
|
|
228
|
+
* const { count, name } = useStoreSelector(store, ['count', 'name']);
|
|
229
|
+
*
|
|
230
|
+
* // Custom comparison for complex objects
|
|
231
|
+
* const { tasks } = useStoreSelector(store, [
|
|
232
|
+
* { tasks: (prev, next) => prev.length === next.length }
|
|
233
|
+
* ]);
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
114
236
|
export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
|
|
115
237
|
store: StoreType<T>,
|
|
116
238
|
selector: S
|
|
@@ -122,6 +244,19 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
|
|
|
122
244
|
const isFirstRunRef = useRef(true);
|
|
123
245
|
const lastValues = useRef<Partial<T>>({});
|
|
124
246
|
const subscribeRef = useRef<((onStoreChange: () => void) => () => void) | null>(null);
|
|
247
|
+
const storeRef = useRef(store);
|
|
248
|
+
|
|
249
|
+
const storeChanged = storeRef.current !== store;
|
|
250
|
+
if (storeChanged) {
|
|
251
|
+
storeRef.current = store;
|
|
252
|
+
lastSelected.current = {};
|
|
253
|
+
prevSelector.current = null;
|
|
254
|
+
normalizedRef.current = null;
|
|
255
|
+
keysRef.current = null;
|
|
256
|
+
isFirstRunRef.current = true;
|
|
257
|
+
lastValues.current = {};
|
|
258
|
+
subscribeRef.current = null;
|
|
259
|
+
}
|
|
125
260
|
|
|
126
261
|
if (!prevSelector.current || !shallowEqualSelector(prevSelector.current, selector)) {
|
|
127
262
|
const normalized: NormalizedSelector<T>[] = [];
|
|
@@ -172,7 +307,7 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
|
|
|
172
307
|
for (const { key, compare } of normalized) {
|
|
173
308
|
const prevVal = lastValues.current[key];
|
|
174
309
|
const nextVal = current[key];
|
|
175
|
-
if (prevVal === undefined ? true : (compare
|
|
310
|
+
if (prevVal === undefined ? true : (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal))) {
|
|
176
311
|
return true;
|
|
177
312
|
}
|
|
178
313
|
}
|
|
@@ -212,10 +347,110 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
|
|
|
212
347
|
return result as Picked<T, S>;
|
|
213
348
|
}, [keys]);
|
|
214
349
|
|
|
215
|
-
if (!subscribeRef.current) {
|
|
350
|
+
if (!subscribeRef.current || storeChanged) {
|
|
216
351
|
subscribeRef.current = (onStoreChange: () => void) =>
|
|
217
352
|
store.subscribe(keys, onStoreChange);
|
|
218
353
|
}
|
|
219
354
|
|
|
220
355
|
return useSyncExternalStore(subscribeRef.current, getSnapshot, () => staticSnapshot);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Interface for storage objects compatible with persistence middleware.
|
|
361
|
+
* Includes localStorage, sessionStorage, AsyncStorage, or any custom storage.
|
|
362
|
+
*/
|
|
363
|
+
export interface StorageSupportingInterface {
|
|
364
|
+
getItem(key: string): string | null;
|
|
365
|
+
setItem(key: string, value: string): void;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Creates a persistence middleware that saves individual keys to storage.
|
|
370
|
+
* Only writes when the specified keys actually change, using per-key storage.
|
|
371
|
+
* Storage format: `${persistKey}:${keyName}` for each persisted key.
|
|
372
|
+
*
|
|
373
|
+
* @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
|
|
374
|
+
* @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
|
|
375
|
+
* @param keys - Array of state keys to persist
|
|
376
|
+
* @returns Tuple of [middleware function, affected keys] for use with addMiddleware
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* // Add persistence for theme and user settings
|
|
381
|
+
* store.addMiddleware(
|
|
382
|
+
* createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
|
|
383
|
+
* );
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
export function createPersistenceMiddleware<T extends object>(
|
|
387
|
+
storage: Storage | StorageSupportingInterface,
|
|
388
|
+
persistKey: string,
|
|
389
|
+
keys: (keyof T)[]
|
|
390
|
+
): [MiddlewareFunction<T>, (keyof T)[]] {
|
|
391
|
+
const middlewareFunction: MiddlewareFunction<T> = (_, update, next) => {
|
|
392
|
+
// Check if any of the persisted keys are being updated
|
|
393
|
+
const changedKeys = keys.filter(key => key in update);
|
|
394
|
+
if (changedKeys.length === 0) {
|
|
395
|
+
return next();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Save each changed key individually
|
|
399
|
+
for (const key of changedKeys) {
|
|
400
|
+
try {
|
|
401
|
+
const value = update[key];
|
|
402
|
+
const storageKey = `${persistKey}:${String(key)}`;
|
|
403
|
+
storage.setItem(storageKey, JSON.stringify(value));
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.warn(`Failed to persist key ${String(key)}:`, error);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
next();
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return [middlewareFunction, keys];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Loads persisted state from individual key storage during store initialization.
|
|
417
|
+
* Reads keys saved by createPersistenceMiddleware and returns them as partial state.
|
|
418
|
+
*
|
|
419
|
+
* @param storage - Storage interface to read from (same as used in middleware)
|
|
420
|
+
* @param persistKey - Base key prefix used for storage (same as used in middleware)
|
|
421
|
+
* @param keys - Array of keys to restore (should match middleware keys)
|
|
422
|
+
* @returns Partial state object with persisted values, or empty object if loading fails
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* // Load persisted state before creating store
|
|
427
|
+
* const persistedState = loadPersistedState(localStorage, 'myapp', ['theme', 'isLoggedIn']);
|
|
428
|
+
*
|
|
429
|
+
* const store = createStoreState({
|
|
430
|
+
* theme: 'light',
|
|
431
|
+
* isLoggedIn: false,
|
|
432
|
+
* ...persistedState // Apply persisted values
|
|
433
|
+
* });
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export function loadPersistedState<T extends object>(
|
|
437
|
+
storage: Storage | StorageSupportingInterface,
|
|
438
|
+
persistKey: string,
|
|
439
|
+
keys: (keyof T)[]
|
|
440
|
+
): Partial<T> {
|
|
441
|
+
const result: Partial<T> = {};
|
|
442
|
+
|
|
443
|
+
for (const key of keys) {
|
|
444
|
+
try {
|
|
445
|
+
const storageKey = `${persistKey}:${String(key)}`;
|
|
446
|
+
const stored = storage.getItem(storageKey);
|
|
447
|
+
if (stored !== null) {
|
|
448
|
+
result[key] = JSON.parse(stored);
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.warn(`Failed to load persisted key ${String(key)}:`, error);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return result;
|
|
221
456
|
}
|