dev-react-microstore 5.0.0 → 6.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 +187 -220
- package/dist/index.d.mts +60 -16
- package/dist/index.d.ts +60 -16
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +16 -6
- package/src/hooks.test.tsx +271 -0
- package/src/index.ts +312 -122
- package/src/store.test.ts +997 -0
- package/src/types.test.ts +161 -0
- package/vitest.config.ts +10 -0
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ type MiddlewareFunction<T extends object> = (currentState: T, update: Partial<T>
|
|
|
4
4
|
* Creates a new reactive store with fine-grained subscriptions and middleware support.
|
|
5
5
|
*
|
|
6
6
|
* @param initialState - The initial state object for the store
|
|
7
|
-
* @returns Store object with methods: get, set, subscribe, select, addMiddleware
|
|
7
|
+
* @returns Store object with methods: get, set, subscribe, select, addMiddleware, onChange
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
@@ -15,10 +15,20 @@ type MiddlewareFunction<T extends object> = (currentState: T, update: Partial<T>
|
|
|
15
15
|
*/
|
|
16
16
|
declare function createStoreState<T extends object>(initialState: T): {
|
|
17
17
|
get: () => T;
|
|
18
|
-
|
|
18
|
+
getKey: <K extends keyof T>(key: K) => T[K];
|
|
19
|
+
set: (update: Partial<T>) => void;
|
|
20
|
+
setKey: <K extends keyof T>(key: K, value: T[K]) => void;
|
|
21
|
+
merge: <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never) => T[K];
|
|
22
|
+
mergeSet: <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never) => void;
|
|
23
|
+
reset: (keys?: (keyof T)[]) => void;
|
|
24
|
+
batch: (fn: () => void) => void;
|
|
19
25
|
subscribe: (keys: (keyof T)[], listener: StoreListener) => (() => void);
|
|
20
26
|
select: <K extends keyof T>(keys: K[]) => Pick<T, K>;
|
|
21
27
|
addMiddleware: (callbackOrTuple: MiddlewareFunction<T> | [MiddlewareFunction<T>, (keyof T)[]], affectedKeys?: (keyof T)[] | null) => () => void;
|
|
28
|
+
onChange: <K extends keyof T>(keys: K[], callback: (values: Pick<T, K>, prev: Pick<T, K>) => void) => (() => void);
|
|
29
|
+
skipSetWhen: <K extends keyof T>(key: K, fn: (prev: T[K], next: T[K]) => boolean) => void;
|
|
30
|
+
removeSkipSetWhen: (key: keyof T) => void;
|
|
31
|
+
_eqReg: Record<string, ((prev: any, next: any) => boolean) | undefined>;
|
|
22
32
|
};
|
|
23
33
|
type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
|
|
24
34
|
type PrimitiveKey<T extends object> = keyof T;
|
|
@@ -53,18 +63,45 @@ type Picked<T extends object, S extends SelectorInput<T>> = ExtractSelectorKeys<
|
|
|
53
63
|
*/
|
|
54
64
|
declare function useStoreSelector<T extends object, S extends SelectorInput<T>>(store: StoreType<T>, selector: S): Picked<T, S>;
|
|
55
65
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* Creates a pre-bound selector hook for a specific store instance.
|
|
67
|
+
* Infers the state type from the store — no manual generics needed.
|
|
68
|
+
*
|
|
69
|
+
* @param store - The store created with createStoreState
|
|
70
|
+
* @returns A React hook with the same API as useStoreSelector, but with the store already bound
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const useMyStore = createSelectorHook(myStore);
|
|
75
|
+
*
|
|
76
|
+
* // In a component:
|
|
77
|
+
* const { count, name } = useMyStore(['count', 'name']);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare function createSelectorHook<T extends object>(store: StoreType<T>): <S extends SelectorInput<T>>(selector: S) => Picked<T, S>;
|
|
81
|
+
/**
|
|
82
|
+
* Interface for synchronous storage (localStorage, sessionStorage, etc.).
|
|
58
83
|
*/
|
|
59
84
|
interface StorageSupportingInterface {
|
|
60
85
|
getItem(key: string): string | null;
|
|
61
86
|
setItem(key: string, value: string): void;
|
|
62
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Interface for asynchronous storage (React Native AsyncStorage, etc.).
|
|
90
|
+
*/
|
|
91
|
+
interface AsyncStorageSupportingInterface {
|
|
92
|
+
getItem(key: string): Promise<string | null>;
|
|
93
|
+
setItem(key: string, value: string): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
type AnyStorage = Storage | StorageSupportingInterface | AsyncStorageSupportingInterface;
|
|
63
96
|
/**
|
|
64
97
|
* Creates a persistence middleware that saves individual keys to storage.
|
|
65
98
|
* Only writes when the specified keys actually change, using per-key storage.
|
|
66
99
|
* Storage format: `${persistKey}:${keyName}` for each persisted key.
|
|
67
100
|
*
|
|
101
|
+
* Works with both synchronous storage (localStorage) and asynchronous storage
|
|
102
|
+
* (React Native AsyncStorage). Async writes are fire-and-forget — the state
|
|
103
|
+
* update is never blocked by a slow write.
|
|
104
|
+
*
|
|
68
105
|
* @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
|
|
69
106
|
* @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
|
|
70
107
|
* @param keys - Array of state keys to persist
|
|
@@ -72,34 +109,41 @@ interface StorageSupportingInterface {
|
|
|
72
109
|
*
|
|
73
110
|
* @example
|
|
74
111
|
* ```ts
|
|
75
|
-
* //
|
|
112
|
+
* // Sync — localStorage
|
|
76
113
|
* store.addMiddleware(
|
|
77
114
|
* createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
|
|
78
115
|
* );
|
|
116
|
+
*
|
|
117
|
+
* // Async — React Native AsyncStorage
|
|
118
|
+
* store.addMiddleware(
|
|
119
|
+
* createPersistenceMiddleware(AsyncStorage, 'myapp', ['theme', 'isLoggedIn'])
|
|
120
|
+
* );
|
|
79
121
|
* ```
|
|
80
122
|
*/
|
|
81
|
-
declare function createPersistenceMiddleware<T extends object>(storage:
|
|
123
|
+
declare function createPersistenceMiddleware<T extends object>(storage: AnyStorage, persistKey: string, keys: (keyof T)[]): [MiddlewareFunction<T>, (keyof T)[]];
|
|
82
124
|
/**
|
|
83
125
|
* Loads persisted state from individual key storage during store initialization.
|
|
84
126
|
* Reads keys saved by createPersistenceMiddleware and returns them as partial state.
|
|
85
127
|
*
|
|
128
|
+
* Returns synchronously for sync storage and a Promise for async storage.
|
|
129
|
+
*
|
|
86
130
|
* @param storage - Storage interface to read from (same as used in middleware)
|
|
87
131
|
* @param persistKey - Base key prefix used for storage (same as used in middleware)
|
|
88
132
|
* @param keys - Array of keys to restore (should match middleware keys)
|
|
89
|
-
* @returns Partial state object
|
|
133
|
+
* @returns Partial state object (sync) or Promise of partial state (async)
|
|
90
134
|
*
|
|
91
135
|
* @example
|
|
92
136
|
* ```ts
|
|
93
|
-
* //
|
|
94
|
-
* const
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* });
|
|
137
|
+
* // Sync — localStorage
|
|
138
|
+
* const persisted = loadPersistedState(localStorage, 'myapp', ['theme']);
|
|
139
|
+
* const store = createStoreState({ theme: 'light', ...persisted });
|
|
140
|
+
*
|
|
141
|
+
* // Async — React Native AsyncStorage
|
|
142
|
+
* const persisted = await loadPersistedState(AsyncStorage, 'myapp', ['theme']);
|
|
143
|
+
* const store = createStoreState({ theme: 'light', ...persisted });
|
|
101
144
|
* ```
|
|
102
145
|
*/
|
|
103
146
|
declare function loadPersistedState<T extends object>(storage: Storage | StorageSupportingInterface, persistKey: string, keys: (keyof T)[]): Partial<T>;
|
|
147
|
+
declare function loadPersistedState<T extends object>(storage: AsyncStorageSupportingInterface, persistKey: string, keys: (keyof T)[]): Promise<Partial<T>>;
|
|
104
148
|
|
|
105
|
-
export { type StorageSupportingInterface, createPersistenceMiddleware, createStoreState, loadPersistedState, useStoreSelector };
|
|
149
|
+
export { type AsyncStorageSupportingInterface, type StorageSupportingInterface, createPersistenceMiddleware, createSelectorHook, createStoreState, loadPersistedState, useStoreSelector };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var F=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames,R=Object.getOwnPropertySymbols;var A=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable;var V=(o,i,r)=>i in o?F(o,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):o[i]=r,h=(o,i)=>{for(var r in i||(i={}))A.call(i,r)&&V(o,r,i[r]);if(R)for(var r of R(i))L.call(i,r)&&V(o,r,i[r]);return o};var N=(o,i)=>{for(var r in i)F(o,r,{get:i[r],enumerable:!0})},W=(o,i,r,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let l of q(i))!A.call(o,l)&&l!==r&&F(o,l,{get:()=>i[l],enumerable:!(t=$(i,l))||t.enumerable});return o};var _=o=>W(F({},"__esModule",{value:!0}),o);var Q={};N(Q,{createPersistenceMiddleware:()=>D,createSelectorHook:()=>B,createStoreState:()=>J,loadPersistedState:()=>G,useStoreSelector:()=>z});module.exports=_(Q);var I=require("react");function J(o){let i=h({},o),r=o,t=Object.create(null),l=[],f=Object.create(null),S=null,m=()=>r,k=e=>r[e],p=e=>{var n;if(S){S.add(e);return}(n=t[e])==null||n.forEach(s=>s())},u=(e,n)=>{if(Object.is(r[e],n))return;let s=f[e];s&&s(r[e],n)||(r[e]=n,p(e))},d=(e,n)=>{if(l.length===0){u(e,n);return}let s={};s[e]=n,b(s)},y=(e,n)=>h(h({},r[e]),n),K=(e,n)=>{if(l.length===0){u(e,h(h({},r[e]),n));return}let s={};s[e]=h(h({},r[e]),n),b(s)},P=e=>{if(!e)b(h({},i));else{let n={};for(let s of e)n[s]=i[s];b(n)}},x=e=>{var n;if(S){e();return}S=new Set;try{e()}finally{let s=S;S=null;let c=new Set;for(let T of s)(n=t[T])==null||n.forEach(a=>{c.has(a)||(c.add(a),a())})}},b=e=>{if(!e)return;if(l.length===0){v(e);return}let n=e,s=0,c=!1,T=a=>{if(a!==void 0&&(n=a),s>=l.length){c||v(n);return}let g=l[s++];if(!g.keys||g.keys.some(w=>w in n)){let w=!1,j=M=>{w||(w=!0,T(M))};try{g.callback(r,n,j)}catch(M){c=!0,console.error("Middleware error:",M);return}if(!w){c=!0;return}}else T()};T()},v=e=>{var s,c;let n=[];for(let T in e){let a=T;if(Object.is(r[a],e[a]))continue;let g=f[T];g&&g(r[a],e[a])||(r[a]=e[a],n.push(T))}if(n.length!==0){if(S){for(let T of n)S.add(T);return}if(n.length===1)(s=t[n[0]])==null||s.forEach(T=>T());else{let T=new Set;for(let a of n)(c=t[a])==null||c.forEach(g=>{T.has(g)||(T.add(g),g())})}}},O=(e,n=null)=>{let s,c;Array.isArray(e)?[s,c]=e:(s=e,c=n);let T={callback:s,keys:c};return l.push(T),()=>{let a=l.indexOf(T);a>-1&&l.splice(a,1)}},C=(e,n)=>{for(let s of e){let c=s;t[c]||(t[c]=new Set),t[c].add(n)}return()=>{var s;for(let c of e)(s=t[c])==null||s.delete(n)}};return{get:m,getKey:k,set:b,setKey:d,merge:y,mergeSet:K,reset:P,batch:x,subscribe:C,select:e=>{let n={},s=r;for(let c of e)n[c]=s[c];return n},addMiddleware:O,onChange:(e,n)=>{let s={};for(let a of e)s[a]=r[a];let c=!1;return C(e,()=>{c||(c=!0,queueMicrotask(()=>{c=!1;let a=!1;for(let j of e)if(!Object.is(r[j],s[j])){a=!0;break}if(!a)return;let g={};for(let j of e)g[j]=r[j];let w=s;s=g,n(g,w)}))})},skipSetWhen:(e,n)=>{f[e]=n},removeSkipSetWhen:e=>{delete f[e]},_eqReg:f}}function H(o,i){return o.length===i.length&&o.every((r,t)=>r===i[t])}function z(o,i){let t=(0,I.useRef)({lastSelected:{},prevSelector:null,normalized:null,keys:null,isFirstRun:!0,lastValues:{},subscribe:null,store:o}).current,l=t.store!==o;if(l&&(t.store=o,t.lastSelected={},t.prevSelector=null,t.normalized=null,t.keys=null,t.isFirstRun=!0,t.lastValues={},t.subscribe=null),!t.prevSelector||!H(t.prevSelector,i)){let p=[],u=[];for(let d of i)if(typeof d=="string"){let y=d;p.push({key:y}),u.push(y)}else{let y=d;for(let K in y){let P=y[K],x=K;p.push({key:x,compare:P}),u.push(x)}}t.normalized=p,t.keys=u,t.prevSelector=i,t.subscribe=null}let f=t.normalized,S=t.keys,m=()=>{let p=o.get();if(t.isFirstRun){t.isFirstRun=!1;let d={};for(let{key:y}of f){let K=p[y];t.lastValues[y]=K,d[y]=K}return t.lastSelected=d,d}let u=null;for(let d=0;d<f.length;d++){let{key:y,compare:K}=f[d],P=t.lastValues[y],x=p[y];if(!Object.is(P,x)){let b=K||o._eqReg[y];if(!b||!b(P,x)){if(!u){u={};for(let v=0;v<d;v++)u[f[v].key]=t.lastValues[f[v].key]}t.lastValues[y]=x,u[y]=x;continue}}u&&(u[y]=P)}return u?(t.lastSelected=u,u):t.lastSelected},k=(0,I.useMemo)(()=>{let p=o.get(),u={};for(let d of S)u[d]=p[d];return u},[S]);return(!t.subscribe||l)&&(t.subscribe=p=>o.subscribe(S,p)),(0,I.useSyncExternalStore)(t.subscribe,m,()=>k)}function B(o){return function(i){return z(o,i)}}function E(o){return o!=null&&typeof o.then=="function"}function D(o,i,r){return[(l,f,S)=>{let m=r.filter(k=>k in f);if(m.length===0)return S();for(let k of m)try{let p=f[k],u=`${i}:${String(k)}`,d=o.setItem(u,JSON.stringify(p));E(d)&&d.catch(y=>{console.warn(`Failed to persist key ${String(k)}:`,y)})}catch(p){console.warn(`Failed to persist key ${String(k)}:`,p)}S()},r]}function G(o,i,r){let t={},l=[];for(let f of r){let S=`${i}:${String(f)}`;try{let m=o.getItem(S);E(m)?l.push(m.then(k=>{k!==null&&(t[f]=JSON.parse(k))}).catch(k=>{console.warn(`Failed to load persisted key ${String(f)}:`,k)})):m!==null&&(t[f]=JSON.parse(m))}catch(m){console.warn(`Failed to load persisted key ${String(f)}:`,m)}}return l.length>0?Promise.all(l).then(()=>t):t}0&&(module.exports={createPersistenceMiddleware,createSelectorHook,createStoreState,loadPersistedState,useStoreSelector});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useMemo as
|
|
1
|
+
var A=Object.defineProperty;var M=Object.getOwnPropertySymbols;var z=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var C=(s,i,o)=>i in s?A(s,i,{enumerable:!0,configurable:!0,writable:!0,value:o}):s[i]=o,h=(s,i)=>{for(var o in i||(i={}))z.call(i,o)&&C(s,o,i[o]);if(M)for(var o of M(i))E.call(i,o)&&C(s,o,i[o]);return s};import{useMemo as O,useRef as $,useSyncExternalStore as q}from"react";function G(s){let i=h({},s),o=s,t=Object.create(null),S=[],u=Object.create(null),T=null,m=()=>o,k=e=>o[e],p=e=>{var n;if(T){T.add(e);return}(n=t[e])==null||n.forEach(r=>r())},l=(e,n)=>{if(Object.is(o[e],n))return;let r=u[e];r&&r(o[e],n)||(o[e]=n,p(e))},f=(e,n)=>{if(S.length===0){l(e,n);return}let r={};r[e]=n,b(r)},d=(e,n)=>h(h({},o[e]),n),K=(e,n)=>{if(S.length===0){l(e,h(h({},o[e]),n));return}let r={};r[e]=h(h({},o[e]),n),b(r)},P=e=>{if(!e)b(h({},i));else{let n={};for(let r of e)n[r]=i[r];b(n)}},x=e=>{var n;if(T){e();return}T=new Set;try{e()}finally{let r=T;T=null;let c=new Set;for(let y of r)(n=t[y])==null||n.forEach(a=>{c.has(a)||(c.add(a),a())})}},b=e=>{if(!e)return;if(S.length===0){v(e);return}let n=e,r=0,c=!1,y=a=>{if(a!==void 0&&(n=a),r>=S.length){c||v(n);return}let g=S[r++];if(!g.keys||g.keys.some(w=>w in n)){let w=!1,j=I=>{w||(w=!0,y(I))};try{g.callback(o,n,j)}catch(I){c=!0,console.error("Middleware error:",I);return}if(!w){c=!0;return}}else y()};y()},v=e=>{var r,c;let n=[];for(let y in e){let a=y;if(Object.is(o[a],e[a]))continue;let g=u[y];g&&g(o[a],e[a])||(o[a]=e[a],n.push(y))}if(n.length!==0){if(T){for(let y of n)T.add(y);return}if(n.length===1)(r=t[n[0]])==null||r.forEach(y=>y());else{let y=new Set;for(let a of n)(c=t[a])==null||c.forEach(g=>{y.has(g)||(y.add(g),g())})}}},V=(e,n=null)=>{let r,c;Array.isArray(e)?[r,c]=e:(r=e,c=n);let y={callback:r,keys:c};return S.push(y),()=>{let a=S.indexOf(y);a>-1&&S.splice(a,1)}},F=(e,n)=>{for(let r of e){let c=r;t[c]||(t[c]=new Set),t[c].add(n)}return()=>{var r;for(let c of e)(r=t[c])==null||r.delete(n)}};return{get:m,getKey:k,set:b,setKey:f,merge:d,mergeSet:K,reset:P,batch:x,subscribe:F,select:e=>{let n={},r=o;for(let c of e)n[c]=r[c];return n},addMiddleware:V,onChange:(e,n)=>{let r={};for(let a of e)r[a]=o[a];let c=!1;return F(e,()=>{c||(c=!0,queueMicrotask(()=>{c=!1;let a=!1;for(let j of e)if(!Object.is(o[j],r[j])){a=!0;break}if(!a)return;let g={};for(let j of e)g[j]=o[j];let w=r;r=g,n(g,w)}))})},skipSetWhen:(e,n)=>{u[e]=n},removeSkipSetWhen:e=>{delete u[e]},_eqReg:u}}function L(s,i){return s.length===i.length&&s.every((o,t)=>o===i[t])}function N(s,i){let t=$({lastSelected:{},prevSelector:null,normalized:null,keys:null,isFirstRun:!0,lastValues:{},subscribe:null,store:s}).current,S=t.store!==s;if(S&&(t.store=s,t.lastSelected={},t.prevSelector=null,t.normalized=null,t.keys=null,t.isFirstRun=!0,t.lastValues={},t.subscribe=null),!t.prevSelector||!L(t.prevSelector,i)){let p=[],l=[];for(let f of i)if(typeof f=="string"){let d=f;p.push({key:d}),l.push(d)}else{let d=f;for(let K in d){let P=d[K],x=K;p.push({key:x,compare:P}),l.push(x)}}t.normalized=p,t.keys=l,t.prevSelector=i,t.subscribe=null}let u=t.normalized,T=t.keys,m=()=>{let p=s.get();if(t.isFirstRun){t.isFirstRun=!1;let f={};for(let{key:d}of u){let K=p[d];t.lastValues[d]=K,f[d]=K}return t.lastSelected=f,f}let l=null;for(let f=0;f<u.length;f++){let{key:d,compare:K}=u[f],P=t.lastValues[d],x=p[d];if(!Object.is(P,x)){let b=K||s._eqReg[d];if(!b||!b(P,x)){if(!l){l={};for(let v=0;v<f;v++)l[u[v].key]=t.lastValues[u[v].key]}t.lastValues[d]=x,l[d]=x;continue}}l&&(l[d]=P)}return l?(t.lastSelected=l,l):t.lastSelected},k=O(()=>{let p=s.get(),l={};for(let f of T)l[f]=p[f];return l},[T]);return(!t.subscribe||S)&&(t.subscribe=p=>s.subscribe(T,p)),q(t.subscribe,m,()=>k)}function Q(s){return function(i){return N(s,i)}}function R(s){return s!=null&&typeof s.then=="function"}function X(s,i,o){return[(S,u,T)=>{let m=o.filter(k=>k in u);if(m.length===0)return T();for(let k of m)try{let p=u[k],l=`${i}:${String(k)}`,f=s.setItem(l,JSON.stringify(p));R(f)&&f.catch(d=>{console.warn(`Failed to persist key ${String(k)}:`,d)})}catch(p){console.warn(`Failed to persist key ${String(k)}:`,p)}T()},o]}function Y(s,i,o){let t={},S=[];for(let u of o){let T=`${i}:${String(u)}`;try{let m=s.getItem(T);R(m)?S.push(m.then(k=>{k!==null&&(t[u]=JSON.parse(k))}).catch(k=>{console.warn(`Failed to load persisted key ${String(u)}:`,k)})):m!==null&&(t[u]=JSON.parse(m))}catch(m){console.warn(`Failed to load persisted key ${String(u)}:`,m)}}return S.length>0?Promise.all(S).then(()=>t):t}export{X as createPersistenceMiddleware,Q as createSelectorHook,G as createStoreState,Y as loadPersistedState,N as useStoreSelector};
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-react-microstore",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "A minimal global state manager for React with fine-grained subscriptions.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/OhadBaehr/react-microstore.git"
|
|
9
|
+
"url": "https://github.com/OhadBaehr/dev-react-microstore.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/OhadBaehr/react-microstore",
|
|
11
|
+
"homepage": "https://github.com/OhadBaehr/dev-react-microstore",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/OhadBaehr/react-microstore/issues"
|
|
13
|
+
"url": "https://github.com/OhadBaehr/dev-react-microstore/issues"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsup src/index.ts --format esm,cjs --dts --minify",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"typecheck": "tsc --noEmit --skipLibCheck",
|
|
17
20
|
"prepare": "npm run build"
|
|
18
21
|
},
|
|
19
22
|
"keywords": [
|
|
@@ -32,8 +35,15 @@
|
|
|
32
35
|
"react": ">=17.0.0"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
38
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
39
|
+
"@testing-library/react": "^16.3.2",
|
|
35
40
|
"@types/react": "^19.1.2",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"jsdom": "^28.1.0",
|
|
43
|
+
"react": "^19.2.4",
|
|
44
|
+
"react-dom": "^19.2.4",
|
|
36
45
|
"tsup": "^8.4.0",
|
|
37
|
-
"typescript": "^5.8.3"
|
|
46
|
+
"typescript": "^5.8.3",
|
|
47
|
+
"vitest": "^4.0.18"
|
|
38
48
|
}
|
|
39
|
-
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import { renderHook, act } from '@testing-library/react'
|
|
4
|
+
import {
|
|
5
|
+
createStoreState,
|
|
6
|
+
useStoreSelector,
|
|
7
|
+
createSelectorHook,
|
|
8
|
+
} from './index'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// useStoreSelector
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
describe('useStoreSelector', () => {
|
|
14
|
+
it('returns selected keys from the store', () => {
|
|
15
|
+
const store = createStoreState({ a: 1, b: 2, c: 3 })
|
|
16
|
+
const { result } = renderHook(() => useStoreSelector(store, ['a', 'c']))
|
|
17
|
+
|
|
18
|
+
expect(result.current).toEqual({ a: 1, c: 3 })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('re-renders when a subscribed key changes', () => {
|
|
22
|
+
const store = createStoreState({ count: 0 })
|
|
23
|
+
const { result } = renderHook(() => useStoreSelector(store, ['count']))
|
|
24
|
+
|
|
25
|
+
expect(result.current.count).toBe(0)
|
|
26
|
+
|
|
27
|
+
act(() => store.set({ count: 5 }))
|
|
28
|
+
|
|
29
|
+
expect(result.current.count).toBe(5)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('does not re-render when an unrelated key changes', () => {
|
|
33
|
+
const store = createStoreState({ a: 1, b: 2 })
|
|
34
|
+
const renderCount = vi.fn()
|
|
35
|
+
|
|
36
|
+
renderHook(() => {
|
|
37
|
+
renderCount()
|
|
38
|
+
return useStoreSelector(store, ['a'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const callsAfterMount = renderCount.mock.calls.length
|
|
42
|
+
|
|
43
|
+
act(() => store.set({ b: 99 }))
|
|
44
|
+
|
|
45
|
+
expect(renderCount.mock.calls.length).toBe(callsAfterMount)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not re-render when set to the same value', () => {
|
|
49
|
+
const store = createStoreState({ count: 0 })
|
|
50
|
+
const renderCount = vi.fn()
|
|
51
|
+
|
|
52
|
+
renderHook(() => {
|
|
53
|
+
renderCount()
|
|
54
|
+
return useStoreSelector(store, ['count'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const callsAfterMount = renderCount.mock.calls.length
|
|
58
|
+
|
|
59
|
+
act(() => store.set({ count: 0 }))
|
|
60
|
+
|
|
61
|
+
expect(renderCount.mock.calls.length).toBe(callsAfterMount)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('handles multiple keys and only re-renders for actual changes', () => {
|
|
65
|
+
const store = createStoreState({ x: 1, y: 2, z: 3 })
|
|
66
|
+
const { result } = renderHook(() => useStoreSelector(store, ['x', 'y']))
|
|
67
|
+
|
|
68
|
+
act(() => store.set({ x: 10 }))
|
|
69
|
+
expect(result.current).toEqual({ x: 10, y: 2 })
|
|
70
|
+
|
|
71
|
+
act(() => store.set({ y: 20 }))
|
|
72
|
+
expect(result.current).toEqual({ x: 10, y: 20 })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('works with custom comparison functions', () => {
|
|
76
|
+
const store = createStoreState({
|
|
77
|
+
items: [1, 2, 3],
|
|
78
|
+
})
|
|
79
|
+
const renderCount = vi.fn()
|
|
80
|
+
|
|
81
|
+
const { result } = renderHook(() => {
|
|
82
|
+
renderCount()
|
|
83
|
+
return useStoreSelector(store, [
|
|
84
|
+
{ items: (prev: number[], next: number[]) => prev.length === next.length },
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const callsAfterMount = renderCount.mock.calls.length
|
|
89
|
+
|
|
90
|
+
// Same length array — custom compare returns true (equal), so no re-render
|
|
91
|
+
act(() => store.set({ items: [4, 5, 6] }))
|
|
92
|
+
expect(renderCount.mock.calls.length).toBe(callsAfterMount)
|
|
93
|
+
|
|
94
|
+
// Different length — custom compare returns false (not equal), triggers re-render
|
|
95
|
+
act(() => store.set({ items: [1, 2, 3, 4] }))
|
|
96
|
+
expect(result.current.items).toEqual([1, 2, 3, 4])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns referentially stable result when nothing changed', () => {
|
|
100
|
+
const store = createStoreState({ a: 1, b: 2 })
|
|
101
|
+
const { result, rerender } = renderHook(() => useStoreSelector(store, ['a']))
|
|
102
|
+
|
|
103
|
+
const first = result.current
|
|
104
|
+
rerender()
|
|
105
|
+
const second = result.current
|
|
106
|
+
|
|
107
|
+
expect(first).toBe(second)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('handles rapid sequential updates correctly', () => {
|
|
111
|
+
const store = createStoreState({ v: 0 })
|
|
112
|
+
const { result } = renderHook(() => useStoreSelector(store, ['v']))
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
store.set({ v: 1 })
|
|
116
|
+
store.set({ v: 2 })
|
|
117
|
+
store.set({ v: 3 })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(result.current.v).toBe(3)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('works with object values', () => {
|
|
124
|
+
const store = createStoreState({ user: { name: 'Alice', age: 30 } })
|
|
125
|
+
const { result } = renderHook(() => useStoreSelector(store, ['user']))
|
|
126
|
+
|
|
127
|
+
expect(result.current.user).toEqual({ name: 'Alice', age: 30 })
|
|
128
|
+
|
|
129
|
+
const newUser = { name: 'Bob', age: 25 }
|
|
130
|
+
act(() => store.set({ user: newUser }))
|
|
131
|
+
expect(result.current.user).toBe(newUser)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('works with null and undefined values', () => {
|
|
135
|
+
const store = createStoreState<{ v: string | null }>({ v: null })
|
|
136
|
+
const { result } = renderHook(() => useStoreSelector(store, ['v']))
|
|
137
|
+
|
|
138
|
+
expect(result.current.v).toBeNull()
|
|
139
|
+
|
|
140
|
+
act(() => store.set({ v: 'hello' }))
|
|
141
|
+
expect(result.current.v).toBe('hello')
|
|
142
|
+
|
|
143
|
+
act(() => store.set({ v: null }))
|
|
144
|
+
expect(result.current.v).toBeNull()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// createSelectorHook
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
describe('createSelectorHook', () => {
|
|
152
|
+
it('returns a hook that works like useStoreSelector', () => {
|
|
153
|
+
const store = createStoreState({ count: 0, name: 'Alice' })
|
|
154
|
+
const useStore = createSelectorHook(store)
|
|
155
|
+
|
|
156
|
+
const { result } = renderHook(() => useStore(['count', 'name']))
|
|
157
|
+
expect(result.current).toEqual({ count: 0, name: 'Alice' })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('re-renders on subscribed key change', () => {
|
|
161
|
+
const store = createStoreState({ count: 0 })
|
|
162
|
+
const useStore = createSelectorHook(store)
|
|
163
|
+
|
|
164
|
+
const { result } = renderHook(() => useStore(['count']))
|
|
165
|
+
|
|
166
|
+
act(() => store.set({ count: 42 }))
|
|
167
|
+
expect(result.current.count).toBe(42)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not re-render on unrelated key change', () => {
|
|
171
|
+
const store = createStoreState({ a: 1, b: 2 })
|
|
172
|
+
const useStore = createSelectorHook(store)
|
|
173
|
+
const renderCount = vi.fn()
|
|
174
|
+
|
|
175
|
+
renderHook(() => {
|
|
176
|
+
renderCount()
|
|
177
|
+
return useStore(['a'])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const callsAfterMount = renderCount.mock.calls.length
|
|
181
|
+
|
|
182
|
+
act(() => store.set({ b: 99 }))
|
|
183
|
+
|
|
184
|
+
expect(renderCount.mock.calls.length).toBe(callsAfterMount)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('supports custom comparison functions', () => {
|
|
188
|
+
const store = createStoreState({ data: [1, 2] })
|
|
189
|
+
const useStore = createSelectorHook(store)
|
|
190
|
+
const renderCount = vi.fn()
|
|
191
|
+
|
|
192
|
+
renderHook(() => {
|
|
193
|
+
renderCount()
|
|
194
|
+
return useStore([
|
|
195
|
+
{ data: (prev: number[], next: number[]) => prev.length === next.length },
|
|
196
|
+
])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const callsAfterMount = renderCount.mock.calls.length
|
|
200
|
+
|
|
201
|
+
act(() => store.set({ data: [3, 4] })) // same length
|
|
202
|
+
expect(renderCount.mock.calls.length).toBe(callsAfterMount)
|
|
203
|
+
|
|
204
|
+
act(() => store.set({ data: [1] })) // different length
|
|
205
|
+
expect(renderCount.mock.calls.length).toBeGreaterThan(callsAfterMount)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('multiple hooks on the same store work independently', () => {
|
|
209
|
+
const store = createStoreState({ a: 1, b: 2 })
|
|
210
|
+
const useStore = createSelectorHook(store)
|
|
211
|
+
|
|
212
|
+
const { result: r1 } = renderHook(() => useStore(['a']))
|
|
213
|
+
const { result: r2 } = renderHook(() => useStore(['b']))
|
|
214
|
+
|
|
215
|
+
act(() => store.set({ a: 10 }))
|
|
216
|
+
|
|
217
|
+
expect(r1.current.a).toBe(10)
|
|
218
|
+
expect(r2.current.b).toBe(2)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Edge cases
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
describe('edge cases', () => {
|
|
226
|
+
it('useStoreSelector with middleware that blocks — hook reflects blocked state', () => {
|
|
227
|
+
const store = createStoreState({ count: 0 })
|
|
228
|
+
store.addMiddleware((_state, update, next) => {
|
|
229
|
+
if (update.count !== undefined && update.count < 0) return // block negatives
|
|
230
|
+
next()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const { result } = renderHook(() => useStoreSelector(store, ['count']))
|
|
234
|
+
|
|
235
|
+
act(() => store.set({ count: 5 }))
|
|
236
|
+
expect(result.current.count).toBe(5)
|
|
237
|
+
|
|
238
|
+
act(() => store.set({ count: -1 })) // blocked
|
|
239
|
+
expect(result.current.count).toBe(5)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('useStoreSelector with middleware that transforms', () => {
|
|
243
|
+
const store = createStoreState({ name: '' })
|
|
244
|
+
store.addMiddleware((_state, update, next) => {
|
|
245
|
+
if (update.name) next({ name: update.name.trim() })
|
|
246
|
+
else next()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const { result } = renderHook(() => useStoreSelector(store, ['name']))
|
|
250
|
+
|
|
251
|
+
act(() => store.set({ name: ' hello ' }))
|
|
252
|
+
expect(result.current.name).toBe('hello')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('store works correctly after many subscribe/unsubscribe cycles', () => {
|
|
256
|
+
const store = createStoreState({ v: 0 })
|
|
257
|
+
const unsubs: (() => void)[] = []
|
|
258
|
+
|
|
259
|
+
for (let i = 0; i < 100; i++) {
|
|
260
|
+
unsubs.push(store.subscribe(['v'], () => {}))
|
|
261
|
+
}
|
|
262
|
+
for (const unsub of unsubs) {
|
|
263
|
+
unsub()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const listener = vi.fn()
|
|
267
|
+
store.subscribe(['v'], listener)
|
|
268
|
+
store.set({ v: 1 })
|
|
269
|
+
expect(listener).toHaveBeenCalledTimes(1)
|
|
270
|
+
})
|
|
271
|
+
})
|