pocket-state 0.0.8 → 0.0.9
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 +281 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,121 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pocket-state
|
|
2
2
|
|
|
3
|
-
A lightweight
|
|
4
|
-
|
|
3
|
+
A lightweight, typed, and framework-agnostic state management library.
|
|
4
|
+
Supports **selectors**, **middleware**, and **Immer-style updates**.
|
|
5
|
+
Works seamlessly **inside React** with hooks or **outside React** with a simple API.
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## ✨ Features
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
- ⚡ Minimal API – Simple and powerful.
|
|
12
|
+
- 🎯 Selectors – Subscribe to slices of state (store-level and hook-level).
|
|
13
|
+
- 🌀 Immer support – Mutate drafts safely with `setImmer`.
|
|
14
|
+
- 🔌 Framework-agnostic – Works in plain TS/JS and React.
|
|
15
|
+
- 🛠 Middleware – Logging, persistence, batching, devtools bridges, etc.
|
|
16
|
+
- 🔔 Event Emitter – Subscribe to store and custom events.
|
|
17
|
+
- ✅ TypeScript-first – Fully type-safe.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
11
22
|
|
|
12
23
|
```bash
|
|
13
|
-
# npm
|
|
14
24
|
npm install pocket-state
|
|
15
|
-
|
|
16
|
-
# yarn
|
|
25
|
+
# or
|
|
17
26
|
yarn add pocket-state
|
|
18
|
-
|
|
27
|
+
# or
|
|
28
|
+
pnpm add pocket-state
|
|
19
29
|
```
|
|
20
30
|
|
|
21
31
|
---
|
|
22
32
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### `createStore<T>(initialState: T, middlewares?: Middleware<T>[])`
|
|
33
|
+
## 🚀 Usage
|
|
26
34
|
|
|
27
|
-
Create a
|
|
35
|
+
### 1) Create a Store
|
|
28
36
|
|
|
29
37
|
```ts
|
|
30
38
|
import {createStore} from 'pocket-state';
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
interface Counter {
|
|
41
|
+
count: number;
|
|
42
|
+
flag: boolean;
|
|
43
|
+
}
|
|
33
44
|
|
|
34
|
-
const counterStore = createStore<Counter>({
|
|
45
|
+
export const counterStore = createStore<Counter>({
|
|
46
|
+
count: 0,
|
|
47
|
+
flag: false,
|
|
48
|
+
});
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
### 2) Read & Write
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
```ts
|
|
54
|
+
// Read full state
|
|
55
|
+
console.log(counterStore.getValue()); // { count: 0, flag: false }
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
// Read by key
|
|
58
|
+
console.log(counterStore.getValue('count')); // 0
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
import {counterStore} from './counterStore';
|
|
60
|
+
// Update via partial
|
|
61
|
+
counterStore.setValue({flag: true});
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return <Text>Count: {count}</Text>;
|
|
51
|
-
}
|
|
52
|
-
```
|
|
63
|
+
// Update via function
|
|
64
|
+
counterStore.setValue(s => ({count: s.count + 1}));
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
// Async update
|
|
67
|
+
counterStore.setValue(async s => {
|
|
68
|
+
const delta = await Promise.resolve(2);
|
|
69
|
+
return {count: s.count + delta};
|
|
70
|
+
});
|
|
71
|
+
```
|
|
55
72
|
|
|
56
|
-
###
|
|
73
|
+
### 3) Immer Updates
|
|
57
74
|
|
|
58
|
-
|
|
75
|
+
📦 Install immer
|
|
59
76
|
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
active: s.active,
|
|
67
|
-
}));
|
|
68
|
-
return (
|
|
69
|
-
<Text>
|
|
70
|
-
{count} {active ? 'ON' : 'OFF'}
|
|
71
|
-
</Text>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
77
|
+
```bash
|
|
78
|
+
npm install immer
|
|
79
|
+
# or
|
|
80
|
+
yarn add immer
|
|
81
|
+
# or
|
|
82
|
+
pnpm add immer
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
```ts
|
|
86
|
+
counterStore.setImmer(draft => {
|
|
87
|
+
draft.count++;
|
|
88
|
+
draft.flag = !draft.flag;
|
|
89
|
+
});
|
|
90
|
+
```
|
|
77
91
|
|
|
78
|
-
###
|
|
92
|
+
### 4) Reset & Dirty Check
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
```ts
|
|
95
|
+
counterStore.reset(); // reset to initial
|
|
96
|
+
counterStore.reset({count: 10}); // reset with override
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `reset(nextState?)` → reset to initial or custom state
|
|
85
|
-
- `subscribe(listener)` → subscribe to all changes
|
|
86
|
-
- `getNumberOfSubscriber()` → count active subscribers
|
|
98
|
+
console.log(counterStore.isDirty()); // true/false
|
|
99
|
+
```
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
### 5) Subscriptions (Outside React)
|
|
89
102
|
|
|
90
103
|
```ts
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
console.log(state
|
|
104
|
+
// Entire state
|
|
105
|
+
const unsub = counterStore.subscribe(state => {
|
|
106
|
+
console.log('New state:', state);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// With selector (push model)
|
|
110
|
+
const unsubCount = counterStore.subscribe(
|
|
111
|
+
s => s.count,
|
|
112
|
+
count => console.log('Count changed:', count),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// cleanup
|
|
116
|
+
unsub();
|
|
117
|
+
unsubCount();
|
|
94
118
|
```
|
|
95
119
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## Example
|
|
120
|
+
### 6) Using with React
|
|
99
121
|
|
|
100
122
|
```tsx
|
|
101
123
|
import React from 'react';
|
|
102
124
|
import {Text, Button, View} from 'react-native';
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
-
// Create store
|
|
106
|
-
const counterStore = createStore({count: 0});
|
|
125
|
+
import {useStore} from 'pocket-state';
|
|
126
|
+
import {counterStore} from './counterStore';
|
|
107
127
|
|
|
108
|
-
export
|
|
128
|
+
export function CounterComponent() {
|
|
109
129
|
const count = useStore(counterStore, s => s.count);
|
|
110
130
|
|
|
111
131
|
return (
|
|
112
132
|
<View>
|
|
113
133
|
<Text>Count: {count}</Text>
|
|
114
134
|
<Button
|
|
115
|
-
title="
|
|
135
|
+
title="Inc"
|
|
116
136
|
onPress={() => counterStore.setValue(s => ({count: s.count + 1}))}
|
|
117
137
|
/>
|
|
118
|
-
<Button title="Reset" onPress={() => counterStore.reset()} />
|
|
119
138
|
</View>
|
|
120
139
|
);
|
|
121
140
|
}
|
|
@@ -123,4 +142,199 @@ export default function App() {
|
|
|
123
142
|
|
|
124
143
|
---
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
## 🎯 Selectors
|
|
146
|
+
|
|
147
|
+
Selectors let you subscribe to **just part of the state**.
|
|
148
|
+
|
|
149
|
+
### React
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
function FlagDisplay() {
|
|
153
|
+
const flag = useStore(counterStore, s => s.flag);
|
|
154
|
+
return <Text>Flag is {flag ? 'ON' : 'OFF'}</Text>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Non‑React
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Only listen to count changes
|
|
162
|
+
const off = counterStore.subscribe(
|
|
163
|
+
s => s.count,
|
|
164
|
+
c => console.log('Count updated:', c),
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Derived selectors (memoized)
|
|
169
|
+
|
|
170
|
+
For CPU‑heavy derivations, memoize a selector factory:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// simple memo (per invocation)
|
|
174
|
+
const makeExpensiveSelector = () => {
|
|
175
|
+
let lastIn: number | undefined;
|
|
176
|
+
let lastOut: number | undefined;
|
|
177
|
+
return (s: {count: number}) => {
|
|
178
|
+
if (lastIn === s.count && lastOut !== undefined) return lastOut;
|
|
179
|
+
// expensive calculation here
|
|
180
|
+
const out = s.count * 2;
|
|
181
|
+
lastIn = s.count;
|
|
182
|
+
lastOut = out;
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const selectDouble = makeExpensiveSelector();
|
|
188
|
+
const double = useStore(counterStore, selectDouble);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> Tip: When selecting multiple fields, prefer returning an **object** and use a shallow equality helper at the hook, or do slice comparison at the store level.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 🧪 Advanced Usage
|
|
196
|
+
|
|
197
|
+
### A) Multiple stores & cross‑updates
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
interface Auth {
|
|
201
|
+
user?: {id: string; name: string} | null;
|
|
202
|
+
}
|
|
203
|
+
interface Todos {
|
|
204
|
+
items: {id: string; title: string; done: boolean}[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const authStore = createStore<Auth>({user: null});
|
|
208
|
+
export const todoStore = createStore<Todos>({items: []});
|
|
209
|
+
|
|
210
|
+
// react to auth changes
|
|
211
|
+
authStore.subscribe(s => {
|
|
212
|
+
if (!s.user) {
|
|
213
|
+
todoStore.reset({items: []}); // clear todos on logout
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### B) Derived state without reselect libraries
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
type Cart = {items: {id: string; price: number; qty: number}[]};
|
|
222
|
+
export const cartStore = createStore<Cart>({items: []});
|
|
223
|
+
|
|
224
|
+
const selectTotal = (s: Cart) =>
|
|
225
|
+
s.items.reduce((sum, it) => sum + it.price * it.qty, 0);
|
|
226
|
+
|
|
227
|
+
// React:
|
|
228
|
+
const total = useStore(cartStore, selectTotal);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### C) Persist middleware (conceptual)
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import type {Middleware} from 'pocket-state';
|
|
235
|
+
|
|
236
|
+
const persist =
|
|
237
|
+
<T>(key: string): Middleware<T> =>
|
|
238
|
+
(next, get) =>
|
|
239
|
+
patch => {
|
|
240
|
+
next(patch);
|
|
241
|
+
try {
|
|
242
|
+
localStorage.setItem(key, JSON.stringify(get()));
|
|
243
|
+
} catch {}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const store = createStore({count: 0}, [persist('app:store')]);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### D) Logger middleware
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const logger =
|
|
253
|
+
(name = 'store'): Middleware<any> =>
|
|
254
|
+
(next, get) =>
|
|
255
|
+
patch => {
|
|
256
|
+
const prev = get();
|
|
257
|
+
console.log(`[${name}] prev`, prev);
|
|
258
|
+
next(patch);
|
|
259
|
+
console.log(`[${name}] next`, get());
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### E) Push vs Pull model
|
|
264
|
+
|
|
265
|
+
- **Push** (store filters): `store.subscribe(selector, listener)` fires **only when slice changes**.
|
|
266
|
+
Hook can be very light (keep last slice, no equality check).
|
|
267
|
+
- **Pull** (hook filters): store emits on any change; `useStore` runs `selector + equality`.
|
|
268
|
+
Useful if your store lacks selector subscriptions.
|
|
269
|
+
|
|
270
|
+
Pick **one** place to compare slices to avoid double work.
|
|
271
|
+
|
|
272
|
+
### F) Coalesced emits
|
|
273
|
+
|
|
274
|
+
If your store batches emits in a microtask, multiple updates in a burst trigger one notify:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
for (let i = 0; i < 10; i++) {
|
|
278
|
+
counterStore.setValue(s => ({count: s.count + 1}));
|
|
279
|
+
}
|
|
280
|
+
// With coalescing, subscribers run once.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### G) Using outside React (workers, Node, services)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
// service.ts
|
|
287
|
+
import {counterStore} from './counterStore';
|
|
288
|
+
|
|
289
|
+
export function increment() {
|
|
290
|
+
counterStore.setValue(s => ({count: s.count + 1}));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function onCountChange(cb: (n: number) => void) {
|
|
294
|
+
return counterStore.subscribe(s => s.count, cb);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### H) Testing
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import {expect, test} from 'vitest';
|
|
302
|
+
import {counterStore} from './counterStore';
|
|
303
|
+
|
|
304
|
+
test('increments', () => {
|
|
305
|
+
counterStore.reset({count: 0, flag: false});
|
|
306
|
+
counterStore.setValue(s => ({count: s.count + 1}));
|
|
307
|
+
expect(counterStore.getValue('count')).toBe(1);
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### I) Type‑safe key reads with `getValue(key)`
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const c = counterStore.getValue('count'); // typed as number
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 🧩 API Reference
|
|
320
|
+
|
|
321
|
+
### `Store<T>`
|
|
322
|
+
|
|
323
|
+
- `getValue(): T` and `getValue(key: K): T[K]`
|
|
324
|
+
- `setValue(patch | (state) => patch | Promise<patch>)`
|
|
325
|
+
- `setImmer((draft) => void)`
|
|
326
|
+
- `reset(next?: T | Partial<T>)`
|
|
327
|
+
- `subscribe(listener)` and `subscribe(selector, listener)`
|
|
328
|
+
- `isDirty()`
|
|
329
|
+
- `getInitialValue()`
|
|
330
|
+
- `getNumberOfSubscriber()`
|
|
331
|
+
|
|
332
|
+
### `useStore(store, selector?)` (React)
|
|
333
|
+
|
|
334
|
+
Lightweight hook built on `useSyncExternalStore` that subscribes to your store and returns the selected slice. It supports both full‑state and slice subscriptions.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## 📜 License
|
|
339
|
+
|
|
340
|
+
ISC — use it however you like.
|