dev-react-microstore 4.0.1 → 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 CHANGED
@@ -1,188 +1,254 @@
1
- # react-microstore
1
+ # dev-react-microstore
2
2
 
3
- A minimal global state manager for React with fine-grained subscriptions.
3
+ Probably the fastest store library ever created for React.
4
+
5
+ A minimal, zero-dependency global state manager with fine-grained subscriptions, full TypeScript inference, and a tiny footprint (< 2KB minified).
4
6
 
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm install react-microstore
10
+ npm install dev-react-microstore
9
11
  ```
10
12
 
11
- ## Usage
13
+ ## Quick Start
12
14
 
13
15
  ```tsx
14
- import { createStoreState, useStoreSelector } from 'react-microstore';
16
+ import { createStoreState, createSelectorHook } from 'dev-react-microstore';
17
+
18
+ const store = createStoreState({
19
+ count: 0,
20
+ user: { name: 'Alice', age: 30 },
21
+ });
15
22
 
16
- const counterStore = createStoreState({ count: 0 });
23
+ export const useStore = createSelectorHook(store);
17
24
 
18
25
  function Counter() {
19
- const { count } = useStoreSelector(counterStore, ['count']);
20
-
21
- return (
22
- <div>
23
- <p>{count}</p>
24
- <button onClick={() => counterStore.set({ count: count + 1 })}>
25
- Increment
26
- </button>
27
- </div>
28
- );
26
+ const { count } = useStore(['count']);
27
+ return <button onClick={() => store.setKey('count', count + 1)}>{count}</button>;
29
28
  }
30
29
  ```
31
30
 
32
- ## Custom Comparison Function
31
+ One line to create the hook, all types inferred from the store instance — no manual generics.
32
+
33
+ ## API
34
+
35
+ ### `createStoreState(initialState)`
36
+
37
+ Creates a reactive store. Returns:
38
+
39
+ | Method | Description |
40
+ |--------|-------------|
41
+ | `get()` | Returns the full state object |
42
+ | `getKey(key)` | Returns the value of a single key |
43
+ | `set(partial)` | Partially updates state. Middleware runs, listeners fire for changed keys only |
44
+ | `setKey(key, value)` | Sets a single key |
45
+ | `merge(key, partial)` | **Pure read** — returns `{ ...state[key], ...partial }` without writing. Type-safe: only works on object-valued keys |
46
+ | `mergeSet(key, partial)` | `merge` + `set` — shallow-merges and writes. Middleware & listeners fire as normal |
47
+ | `reset(keys?)` | Resets keys to their initial values. No args = full reset |
48
+ | `batch(fn)` | Groups multiple `set`/`setKey`/`mergeSet` calls — listeners fire once at the end |
49
+ | `subscribe(keys, listener)` | Fine-grained subscription. Returns unsubscribe function |
50
+ | `select(keys)` | Returns a `Pick<T, K>` snapshot of specific keys |
51
+ | `onChange(keys, callback)` | Non-React listener with `(newValues, prevValues)` — batched per microtask |
52
+ | `addMiddleware(fn, keys?)` | Express-style middleware to intercept, block, or transform updates |
53
+ | `skipSetWhen(key, fn)` | Skip updates for a key when `fn(prev, next)` returns `true`. Runs after `Object.is` |
54
+ | `removeSkipSetWhen(key)` | Remove the skip condition, restoring default `Object.is` behavior |
55
+
56
+ ### `createSelectorHook(store)`
57
+
58
+ Returns a pre-bound React hook with full type inference:
33
59
 
34
60
  ```tsx
35
- import { createStoreState, useStoreSelector } from 'react-microstore';
36
-
37
- const taskStore = createStoreState({
38
- tasks: [
39
- { id: 1, title: 'Learn React', completed: false, priority: 'high' },
40
- { id: 2, title: 'Build app', completed: false, priority: 'medium' }
41
- ],
42
- filters: {
43
- showCompleted: true,
44
- priorityFilter: null
45
- }
46
- });
61
+ const useStore = createSelectorHook(store);
47
62
 
48
- function TaskList() {
49
- // Only re-render when task completion status changes
50
- const { tasks } = useStoreSelector(taskStore, [
51
- {
52
- tasks: (prev, next) =>
53
- !prev.some((task, i) => task.id === next?.[i]?.id && task.completed !== next[i].completed)
54
- }
55
- ]);
56
-
57
- const toggleTask = (id) => {
58
- const currentTasks = taskStore.get().tasks;
59
- const updatedTasks = currentTasks.map(task =>
60
- task.id === id ? { ...task, completed: !task.completed } : task
61
- );
62
-
63
- taskStore.set({ tasks: updatedTasks });
64
- };
65
-
66
- return (
67
- <ul>
68
- {tasks.map(task => (
69
- <li key={task.id}>
70
- {task.title} - {task.completed ? 'Done' : 'Pending'}
71
- <button onClick={() => toggleTask(task.id)}>
72
- Toggle
73
- </button>
74
- </li>
75
- ))}
76
- </ul>
77
- );
78
- }
63
+ // In a component — types are inferred, no generics needed
64
+ const { user } = useStore(['user']);
65
+ // user is { name: string; age: number }
66
+ ```
67
+
68
+ ### `useStoreSelector(store, selector)`
69
+
70
+ Low-level React hook. Prefer `createSelectorHook` for cleaner usage.
71
+
72
+ ## merge vs mergeSet
73
+
74
+ `merge` is a pure read helper — it returns the merged object without touching the store:
75
+
76
+ ```tsx
77
+ const updated = store.merge('user', { age: 31 });
78
+ // updated = { name: 'Alice', age: 31 }
79
+ // store is unchanged
79
80
  ```
80
81
 
81
- ## Example of setting store from outside components
82
+ `mergeSet` writes it:
82
83
 
83
84
  ```tsx
84
- import { createStoreState, useStoreSelector } from 'react-microstore';
85
+ store.mergeSet('user', { age: 31 });
86
+ // store.user is now { name: 'Alice', age: 31 }
87
+ ```
88
+
89
+ Both are type-safe — calling on a primitive key (e.g. `merge('count', ...)`) is a compile error.
90
+
91
+ ## Batching
85
92
 
86
- // Create store
87
- const userStore = createStoreState({
88
- user: null,
89
- isLoading: false,
90
- error: null
93
+ Group multiple updates so listeners fire once:
94
+
95
+ ```tsx
96
+ store.batch(() => {
97
+ store.setKey('count', 10);
98
+ store.mergeSet('user', { age: 25 });
99
+ store.setKey('name', 'Bob');
91
100
  });
101
+ // Listeners fire once with all changes
102
+ ```
103
+
104
+ ## reset
92
105
 
93
- // Function to update store from anywhere
94
- export async function fetchUserData(userId) {
95
- // Update loading state
96
- userStore.set({ isLoading: true, error: null });
97
-
98
- try {
99
- // API call
100
- const response = await fetch(`/api/users/${userId}`);
101
- const userData = await response.json();
102
-
103
- // Update store with fetched data
104
- userStore.set({
105
- user: userData,
106
- isLoading: false
107
- });
108
-
109
- return userData;
110
- } catch (error) {
111
- // Update store with error
112
- userStore.set({
113
- error: error.message,
114
- isLoading: false
115
- });
116
-
117
- throw error;
106
+ ```tsx
107
+ store.reset(); // Full reset to initial state
108
+ store.reset(['count']); // Reset specific keys
109
+ ```
110
+
111
+ ## Middleware
112
+
113
+ Express-style middleware to intercept, block, or transform updates:
114
+
115
+ ```tsx
116
+ // Validation block negative counts
117
+ store.addMiddleware(
118
+ (state, update, next) => {
119
+ if (update.count !== undefined && update.count < 0) return;
120
+ next();
121
+ },
122
+ ['count']
123
+ );
124
+
125
+ // Transform — normalize user input
126
+ store.addMiddleware((state, update, next) => {
127
+ if (update.user?.name) {
128
+ next({ ...update, user: { ...update.user, name: update.user.name.trim() } });
129
+ } else {
130
+ next();
118
131
  }
119
- }
132
+ });
120
133
 
121
- // Components can use the store
122
- function UserProfile() {
123
- const { user, isLoading, error } = useStoreSelector(userStore, ['user', 'isLoading', 'error']);
124
-
125
- if (isLoading) return <div>Loading...</div>;
126
- if (error) return <div>Error: {error}</div>;
127
- if (!user) return <div>No user data</div>;
128
-
129
- return (
130
- <div>
131
- <h2>{user.name}</h2>
132
- <p>Email: {user.email}</p>
133
- </div>
134
- );
135
- }
134
+ // Logging
135
+ store.addMiddleware((state, update, next) => {
136
+ console.log('Update:', update);
137
+ next();
138
+ });
139
+ ```
140
+
141
+ No-middleware fast path: when no middleware is registered, `set()` skips the pipeline entirely.
142
+
143
+ ## Persistence
144
+
145
+ Built-in middleware for automatic state persistence. Supports both sync (`localStorage`) and async (`AsyncStorage`) backends:
146
+
147
+ ```tsx
148
+ import { createStoreState, createPersistenceMiddleware, loadPersistedState } from 'dev-react-microstore';
149
+
150
+ // Sync (localStorage / sessionStorage)
151
+ const persisted = loadPersistedState<AppState>(localStorage, 'app', ['theme', 'user']);
136
152
 
137
- // Can call the function from anywhere
138
- // fetchUserData('123');
153
+ const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
154
+ store.addMiddleware(createPersistenceMiddleware(localStorage, 'app', ['theme', 'user']));
155
+
156
+ // Async (React Native AsyncStorage)
157
+ const persisted = await loadPersistedState<AppState>(AsyncStorage, 'app', ['theme', 'user']);
158
+
159
+ const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
160
+ store.addMiddleware(createPersistenceMiddleware(AsyncStorage, 'app', ['theme', 'user']));
139
161
  ```
140
162
 
141
- ## Features
163
+ Each key is stored individually (`app:theme`, `app:user`) — no serializing the entire state blob.
164
+
165
+ ## Change Listeners (outside React)
142
166
 
143
- - Extremely lightweight (less than 2KB minified)
144
- - Fine-grained subscriptions to minimize re-renders
145
- - Custom comparison functions for complex state updates
146
- - Fully Type-safe
147
- - No dependencies other than React
148
- - Update store from anywhere in your application
167
+ Listen for value changes anywhere module scope, event handlers, async functions:
149
168
 
150
- ## Development Tools
169
+ ```tsx
170
+ const unsub = store.onChange(['theme', 'locale'], (values, prev) => {
171
+ document.body.className = values.theme;
172
+ console.log(`Theme: ${prev.theme} → ${values.theme}`);
173
+ });
151
174
 
152
- ### ESLint Plugin
175
+ // Multiple synchronous set() calls are batched into one notification
176
+ unsub();
177
+ ```
153
178
 
154
- [eslint-plugin-react-microstore](https://www.npmjs.com/package/eslint-plugin-react-microstore) provides ESLint rules to help you write with react-microstore.
179
+ ## Custom Comparison
155
180
 
156
- #### Installation
181
+ Control when re-renders happen with custom equality functions:
157
182
 
158
- ```bash
159
- npm install --save-dev eslint-plugin-react-microstore
183
+ ```tsx
184
+ const { tasks } = useStore([
185
+ {
186
+ tasks: (prev, next) =>
187
+ !prev.some((t, i) => t.completed !== next?.[i]?.completed)
188
+ }
189
+ ]);
160
190
  ```
161
191
 
162
- #### Usage
192
+ ## skipSetWhen
163
193
 
164
- ESLint configuration:
194
+ Skip updates for a key when a condition is met. `Object.is` always runs first as a fast path; `skipSetWhen` is an additional check when references differ. Return `true` = skip the update:
165
195
 
166
196
  ```tsx
167
- import reactMicrostore from 'eslint-plugin-react-microstore';
168
-
169
- const eslintConfig = [{
170
- "plugins": {
171
- "react-microstore": reactMicrostore
172
- },
173
- "rules": {
174
- "react-microstore/no-unused-selector-keys": "warn"
175
- }
176
- }]
197
+ const store = createStoreState({ user: { id: 1, name: 'Alice' }, tags: ['a', 'b'] });
198
+
199
+ // Skip update if id and name haven't changed
200
+ store.skipSetWhen('user', (prev, next) => prev.id === next.id && prev.name === next.name);
201
+
202
+ // Skip if array contents are the same
203
+ store.skipSetWhen('tags', (prev, next) => prev.length === next.length && prev.every((t, i) => t === next[i]));
204
+
205
+ // mergeSet with identical content is now a no-op — no listeners, no middleware
206
+ store.mergeSet('user', { name: 'Alice' }); // skipped
207
+
208
+ // Remove when no longer needed
209
+ store.removeSkipSetWhen('user');
177
210
  ```
178
211
 
179
- `react-microstore/no-unused-selector-keys`
180
- Warns when you select keys in `useStoreSelector` but don't destructure or use them.
212
+ Type-safe: `prev` and `next` types are inferred from the key.
213
+
214
+ ## Features
215
+
216
+ - Probably the fastest React store library ever created
217
+ - Extremely lightweight (< 2KB minified)
218
+ - Fine-grained subscriptions — components only re-render when their keys change
219
+ - `createSelectorHook` for one-line, fully-typed per-store hooks
220
+ - `merge` / `mergeSet` for ergonomic object updates
221
+ - `batch` to group updates and fire listeners once
222
+ - `reset` to restore initial state (full or per-key)
223
+ - `onChange` for non-React listeners with value tracking
224
+ - `skipSetWhen` to prevent unnecessary updates on object-valued keys
225
+ - Custom comparison functions for complex equality
226
+ - Express-style middleware (validation, transforms, logging)
227
+ - Automatic persistence to localStorage, sessionStorage, or AsyncStorage
228
+ - Fully type-safe — all types inferred from store instance
229
+ - Zero dependencies (peer: React >= 17)
230
+
231
+ ## ESLint Plugin
232
+
233
+ [eslint-plugin-dev-react-microstore](https://www.npmjs.com/package/eslint-plugin-dev-react-microstore) warns on unused selector keys:
234
+
235
+ ```bash
236
+ npm install --save-dev eslint-plugin-dev-react-microstore
237
+ ```
238
+
239
+ ```tsx
240
+ import reactMicrostore from 'eslint-plugin-dev-react-microstore';
241
+
242
+ export default [{
243
+ plugins: { 'dev-react-microstore': reactMicrostore },
244
+ rules: { 'dev-react-microstore/no-unused-selector-keys': 'warn' }
245
+ }];
246
+ ```
181
247
 
182
248
  ```tsx
183
- // ❌ This will trigger the rule
184
- const { a } = useStoreSelector(store, ['a', 'b']); // 'b' is unused
249
+ // ❌ Warns 'b' is selected but unused
250
+ const { a } = useStore(['a', 'b']);
185
251
 
186
- // ✅ This is fine
187
- const { a, b } = useStoreSelector(store, ['a', 'b']);
188
- ```
252
+ // ✅ Fine
253
+ const { a, b } = useStore(['a', 'b']);
254
+ ```
package/dist/index.d.mts CHANGED
@@ -1,9 +1,34 @@
1
1
  type StoreListener = () => void;
2
+ type MiddlewareFunction<T extends object> = (currentState: T, update: Partial<T>, next: (modifiedUpdate?: Partial<T>) => void) => void;
3
+ /**
4
+ * Creates a new reactive store with fine-grained subscriptions and middleware support.
5
+ *
6
+ * @param initialState - The initial state object for the store
7
+ * @returns Store object with methods: get, set, subscribe, select, addMiddleware, onChange
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const store = createStoreState({ count: 0, name: 'John' });
12
+ * store.set({ count: 1 }); // Update state
13
+ * const { count } = useStoreSelector(store, ['count']); // Subscribe in React
14
+ * ```
15
+ */
2
16
  declare function createStoreState<T extends object>(initialState: T): {
3
17
  get: () => T;
4
- set: (next: Partial<T>, debounceDelay?: number | boolean) => void;
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;
5
25
  subscribe: (keys: (keyof T)[], listener: StoreListener) => (() => void);
6
26
  select: <K extends keyof T>(keys: K[]) => Pick<T, K>;
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>;
7
32
  };
8
33
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
9
34
  type PrimitiveKey<T extends object> = keyof T;
@@ -17,6 +42,108 @@ type ExtractSelectorKeys<T extends object, S extends SelectorInput<T>> = {
17
42
  [K in S[number] extends infer Item ? Item extends keyof T ? Item : keyof Item : never]: T[K];
18
43
  };
19
44
  type Picked<T extends object, S extends SelectorInput<T>> = ExtractSelectorKeys<T, S>;
45
+ /**
46
+ * React hook that subscribes to specific keys in a store with fine-grained re-renders.
47
+ * Only re-renders when the selected keys actually change (using Object.is comparison).
48
+ *
49
+ * @param store - The store created with createStoreState
50
+ * @param selector - Array of keys to subscribe to, or objects with custom compare functions
51
+ * @returns Selected state values from the store
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // Subscribe to specific keys
56
+ * const { count, name } = useStoreSelector(store, ['count', 'name']);
57
+ *
58
+ * // Custom comparison for complex objects
59
+ * const { tasks } = useStoreSelector(store, [
60
+ * { tasks: (prev, next) => prev.length === next.length }
61
+ * ]);
62
+ * ```
63
+ */
20
64
  declare function useStoreSelector<T extends object, S extends SelectorInput<T>>(store: StoreType<T>, selector: S): Picked<T, S>;
65
+ /**
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.).
83
+ */
84
+ interface StorageSupportingInterface {
85
+ getItem(key: string): string | null;
86
+ setItem(key: string, value: string): void;
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;
96
+ /**
97
+ * Creates a persistence middleware that saves individual keys to storage.
98
+ * Only writes when the specified keys actually change, using per-key storage.
99
+ * Storage format: `${persistKey}:${keyName}` for each persisted key.
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
+ *
105
+ * @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
106
+ * @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
107
+ * @param keys - Array of state keys to persist
108
+ * @returns Tuple of [middleware function, affected keys] for use with addMiddleware
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Sync — localStorage
113
+ * store.addMiddleware(
114
+ * createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
115
+ * );
116
+ *
117
+ * // Async — React Native AsyncStorage
118
+ * store.addMiddleware(
119
+ * createPersistenceMiddleware(AsyncStorage, 'myapp', ['theme', 'isLoggedIn'])
120
+ * );
121
+ * ```
122
+ */
123
+ declare function createPersistenceMiddleware<T extends object>(storage: AnyStorage, persistKey: string, keys: (keyof T)[]): [MiddlewareFunction<T>, (keyof T)[]];
124
+ /**
125
+ * Loads persisted state from individual key storage during store initialization.
126
+ * Reads keys saved by createPersistenceMiddleware and returns them as partial state.
127
+ *
128
+ * Returns synchronously for sync storage and a Promise for async storage.
129
+ *
130
+ * @param storage - Storage interface to read from (same as used in middleware)
131
+ * @param persistKey - Base key prefix used for storage (same as used in middleware)
132
+ * @param keys - Array of keys to restore (should match middleware keys)
133
+ * @returns Partial state object (sync) or Promise of partial state (async)
134
+ *
135
+ * @example
136
+ * ```ts
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 });
144
+ * ```
145
+ */
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>>;
21
148
 
22
- export { createStoreState, useStoreSelector };
149
+ export { type AsyncStorageSupportingInterface, type StorageSupportingInterface, createPersistenceMiddleware, createSelectorHook, createStoreState, loadPersistedState, useStoreSelector };