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/README.md
CHANGED
|
@@ -1,287 +1,254 @@
|
|
|
1
|
-
# react-microstore
|
|
1
|
+
# dev-react-microstore
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
13
|
+
## Quick Start
|
|
12
14
|
|
|
13
15
|
```tsx
|
|
14
|
-
import { createStoreState,
|
|
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
|
|
23
|
+
export const useStore = createSelectorHook(store);
|
|
17
24
|
|
|
18
25
|
function Counter() {
|
|
19
|
-
const { 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
|
-
|
|
31
|
+
One line to create the hook, all types inferred from the store instance — no manual generics.
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
## API
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
const store = createStoreState({ count: 0, user: null });
|
|
35
|
+
### `createStoreState(initialState)`
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
store.addMiddleware(
|
|
41
|
-
(currentState, update, next) => {
|
|
42
|
-
if (update.count !== undefined && update.count < 0) {
|
|
43
|
-
// Don't call next() to block the update
|
|
44
|
-
console.log('Blocked negative count');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
next(); // Allow the update
|
|
48
|
-
},
|
|
49
|
-
['count'] // Only run for count updates
|
|
50
|
-
);
|
|
37
|
+
Creates a reactive store. Returns:
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
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 |
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
store.addMiddleware(
|
|
72
|
-
(currentState, update, next) => {
|
|
73
|
-
console.log('Processing update:', update);
|
|
74
|
-
next(); // Continue
|
|
75
|
-
}
|
|
76
|
-
);
|
|
77
|
-
```
|
|
56
|
+
### `createSelectorHook(store)`
|
|
78
57
|
|
|
79
|
-
|
|
58
|
+
Returns a pre-bound React hook with full type inference:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
const useStore = createSelectorHook(store);
|
|
80
62
|
|
|
81
|
-
|
|
63
|
+
// In a component — types are inferred, no generics needed
|
|
64
|
+
const { user } = useStore(['user']);
|
|
65
|
+
// user is { name: string; age: number }
|
|
66
|
+
```
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
import { createStoreState, createPersistenceMiddleware, loadPersistedState } from '@ohad/react-microstore'
|
|
68
|
+
### `useStoreSelector(store, selector)`
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
const persistedState = loadPersistedState<AppState>(
|
|
88
|
-
localStorage,
|
|
89
|
-
'my-app-state',
|
|
90
|
-
['theme', 'userName', 'isLoggedIn']
|
|
91
|
-
);
|
|
70
|
+
Low-level React hook. Prefer `createSelectorHook` for cleaner usage.
|
|
92
71
|
|
|
93
|
-
|
|
94
|
-
const store = createStoreState<AppState>({
|
|
95
|
-
theme: 'light',
|
|
96
|
-
userName: '',
|
|
97
|
-
isLoggedIn: false,
|
|
98
|
-
tempData: { cache: [] },
|
|
99
|
-
...persistedState // Apply persisted values
|
|
100
|
-
});
|
|
72
|
+
## merge vs mergeSet
|
|
101
73
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
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
|
|
106
80
|
```
|
|
107
81
|
|
|
108
|
-
|
|
82
|
+
`mergeSet` writes it:
|
|
109
83
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
✅ **Flexible** - Easy to swap storage backends or add custom logic
|
|
84
|
+
```tsx
|
|
85
|
+
store.mergeSet('user', { age: 31 });
|
|
86
|
+
// store.user is now { name: 'Alice', age: 31 }
|
|
87
|
+
```
|
|
115
88
|
|
|
116
|
-
|
|
89
|
+
Both are type-safe — calling on a primitive key (e.g. `merge('count', ...)`) is a compile error.
|
|
117
90
|
|
|
118
|
-
|
|
91
|
+
## Batching
|
|
92
|
+
|
|
93
|
+
Group multiple updates so listeners fire once:
|
|
119
94
|
|
|
120
95
|
```tsx
|
|
121
|
-
|
|
122
|
-
store.
|
|
96
|
+
store.batch(() => {
|
|
97
|
+
store.setKey('count', 10);
|
|
98
|
+
store.mergeSet('user', { age: 25 });
|
|
99
|
+
store.setKey('name', 'Bob');
|
|
100
|
+
});
|
|
101
|
+
// Listeners fire once with all changes
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## reset
|
|
123
105
|
|
|
124
|
-
|
|
125
|
-
store.
|
|
106
|
+
```tsx
|
|
107
|
+
store.reset(); // Full reset to initial state
|
|
108
|
+
store.reset(['count']); // Reset specific keys
|
|
126
109
|
```
|
|
127
110
|
|
|
128
|
-
##
|
|
111
|
+
## Middleware
|
|
112
|
+
|
|
113
|
+
Express-style middleware to intercept, block, or transform updates:
|
|
129
114
|
|
|
130
115
|
```tsx
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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();
|
|
141
131
|
}
|
|
142
132
|
});
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
!prev.some((task, i) => task.id === next?.[i]?.id && task.completed !== next[i].completed)
|
|
150
|
-
}
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
const toggleTask = (id) => {
|
|
154
|
-
const currentTasks = taskStore.get().tasks;
|
|
155
|
-
const updatedTasks = currentTasks.map(task =>
|
|
156
|
-
task.id === id ? { ...task, completed: !task.completed } : task
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
taskStore.set({ tasks: updatedTasks });
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<ul>
|
|
164
|
-
{tasks.map(task => (
|
|
165
|
-
<li key={task.id}>
|
|
166
|
-
{task.title} - {task.completed ? 'Done' : 'Pending'}
|
|
167
|
-
<button onClick={() => toggleTask(task.id)}>
|
|
168
|
-
Toggle
|
|
169
|
-
</button>
|
|
170
|
-
</li>
|
|
171
|
-
))}
|
|
172
|
-
</ul>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
134
|
+
// Logging
|
|
135
|
+
store.addMiddleware((state, update, next) => {
|
|
136
|
+
console.log('Update:', update);
|
|
137
|
+
next();
|
|
138
|
+
});
|
|
175
139
|
```
|
|
176
140
|
|
|
177
|
-
|
|
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:
|
|
178
146
|
|
|
179
147
|
```tsx
|
|
180
|
-
import { createStoreState,
|
|
148
|
+
import { createStoreState, createPersistenceMiddleware, loadPersistedState } from 'dev-react-microstore';
|
|
181
149
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
user: null,
|
|
185
|
-
isLoading: false,
|
|
186
|
-
error: null
|
|
187
|
-
});
|
|
150
|
+
// Sync (localStorage / sessionStorage)
|
|
151
|
+
const persisted = loadPersistedState<AppState>(localStorage, 'app', ['theme', 'user']);
|
|
188
152
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Update loading state
|
|
192
|
-
userStore.set({ isLoading: true, error: null });
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
// API call
|
|
196
|
-
const response = await fetch(`/api/users/${userId}`);
|
|
197
|
-
const userData = await response.json();
|
|
198
|
-
|
|
199
|
-
// Update store with fetched data
|
|
200
|
-
userStore.set({
|
|
201
|
-
user: userData,
|
|
202
|
-
isLoading: false
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return userData;
|
|
206
|
-
} catch (error) {
|
|
207
|
-
// Update store with error
|
|
208
|
-
userStore.set({
|
|
209
|
-
error: error.message,
|
|
210
|
-
isLoading: false
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
153
|
+
const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
|
|
154
|
+
store.addMiddleware(createPersistenceMiddleware(localStorage, 'app', ['theme', 'user']));
|
|
216
155
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
const { user, isLoading, error } = useStoreSelector(userStore, ['user', 'isLoading', 'error']);
|
|
220
|
-
|
|
221
|
-
if (isLoading) return <div>Loading...</div>;
|
|
222
|
-
if (error) return <div>Error: {error}</div>;
|
|
223
|
-
if (!user) return <div>No user data</div>;
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<div>
|
|
227
|
-
<h2>{user.name}</h2>
|
|
228
|
-
<p>Email: {user.email}</p>
|
|
229
|
-
</div>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
156
|
+
// Async (React Native AsyncStorage)
|
|
157
|
+
const persisted = await loadPersistedState<AppState>(AsyncStorage, 'app', ['theme', 'user']);
|
|
232
158
|
|
|
233
|
-
|
|
234
|
-
|
|
159
|
+
const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
|
|
160
|
+
store.addMiddleware(createPersistenceMiddleware(AsyncStorage, 'app', ['theme', 'user']));
|
|
235
161
|
```
|
|
236
162
|
|
|
237
|
-
|
|
163
|
+
Each key is stored individually (`app:theme`, `app:user`) — no serializing the entire state blob.
|
|
164
|
+
|
|
165
|
+
## Change Listeners (outside React)
|
|
238
166
|
|
|
239
|
-
|
|
240
|
-
- Fine-grained subscriptions to minimize re-renders
|
|
241
|
-
- Custom comparison functions for complex state updates
|
|
242
|
-
- Simple middleware support with `addMiddleware()`
|
|
243
|
-
- Automatic persistence to localStorage/sessionStorage
|
|
244
|
-
- Debouncing support to control update frequency
|
|
245
|
-
- Fully Type-safe with TypeScript
|
|
246
|
-
- No dependencies other than React
|
|
247
|
-
- Update store from anywhere in your application
|
|
167
|
+
Listen for value changes anywhere — module scope, event handlers, async functions:
|
|
248
168
|
|
|
249
|
-
|
|
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
|
+
});
|
|
250
174
|
|
|
251
|
-
|
|
175
|
+
// Multiple synchronous set() calls are batched into one notification
|
|
176
|
+
unsub();
|
|
177
|
+
```
|
|
252
178
|
|
|
253
|
-
|
|
179
|
+
## Custom Comparison
|
|
254
180
|
|
|
255
|
-
|
|
181
|
+
Control when re-renders happen with custom equality functions:
|
|
256
182
|
|
|
257
|
-
```
|
|
258
|
-
|
|
183
|
+
```tsx
|
|
184
|
+
const { tasks } = useStore([
|
|
185
|
+
{
|
|
186
|
+
tasks: (prev, next) =>
|
|
187
|
+
!prev.some((t, i) => t.completed !== next?.[i]?.completed)
|
|
188
|
+
}
|
|
189
|
+
]);
|
|
259
190
|
```
|
|
260
191
|
|
|
261
|
-
|
|
192
|
+
## skipSetWhen
|
|
262
193
|
|
|
263
|
-
|
|
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:
|
|
264
195
|
|
|
265
196
|
```tsx
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
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');
|
|
276
210
|
```
|
|
277
211
|
|
|
278
|
-
`
|
|
279
|
-
|
|
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
|
+
```
|
|
280
247
|
|
|
281
248
|
```tsx
|
|
282
|
-
// ❌
|
|
283
|
-
const { a } =
|
|
249
|
+
// ❌ Warns — 'b' is selected but unused
|
|
250
|
+
const { a } = useStore(['a', 'b']);
|
|
284
251
|
|
|
285
|
-
// ✅
|
|
286
|
-
const { a, b } =
|
|
287
|
-
```
|
|
252
|
+
// ✅ Fine
|
|
253
|
+
const { a, b } = useStore(['a', 'b']);
|
|
254
|
+
```
|
package/dist/index.d.mts
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 };
|