pocket-state 0.0.7 → 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 +340 -0
- package/package.json +1 -1
- package/src/README +0 -126
package/README.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# pocket-state
|
|
2
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
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
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install pocket-state
|
|
25
|
+
# or
|
|
26
|
+
yarn add pocket-state
|
|
27
|
+
# or
|
|
28
|
+
pnpm add pocket-state
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Usage
|
|
34
|
+
|
|
35
|
+
### 1) Create a Store
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {createStore} from 'pocket-state';
|
|
39
|
+
|
|
40
|
+
interface Counter {
|
|
41
|
+
count: number;
|
|
42
|
+
flag: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const counterStore = createStore<Counter>({
|
|
46
|
+
count: 0,
|
|
47
|
+
flag: false,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2) Read & Write
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Read full state
|
|
55
|
+
console.log(counterStore.getValue()); // { count: 0, flag: false }
|
|
56
|
+
|
|
57
|
+
// Read by key
|
|
58
|
+
console.log(counterStore.getValue('count')); // 0
|
|
59
|
+
|
|
60
|
+
// Update via partial
|
|
61
|
+
counterStore.setValue({flag: true});
|
|
62
|
+
|
|
63
|
+
// Update via function
|
|
64
|
+
counterStore.setValue(s => ({count: s.count + 1}));
|
|
65
|
+
|
|
66
|
+
// Async update
|
|
67
|
+
counterStore.setValue(async s => {
|
|
68
|
+
const delta = await Promise.resolve(2);
|
|
69
|
+
return {count: s.count + delta};
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3) Immer Updates
|
|
74
|
+
|
|
75
|
+
📦 Install immer
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install immer
|
|
79
|
+
# or
|
|
80
|
+
yarn add immer
|
|
81
|
+
# or
|
|
82
|
+
pnpm add immer
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
counterStore.setImmer(draft => {
|
|
87
|
+
draft.count++;
|
|
88
|
+
draft.flag = !draft.flag;
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4) Reset & Dirty Check
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
counterStore.reset(); // reset to initial
|
|
96
|
+
counterStore.reset({count: 10}); // reset with override
|
|
97
|
+
|
|
98
|
+
console.log(counterStore.isDirty()); // true/false
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5) Subscriptions (Outside React)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
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();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 6) Using with React
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import React from 'react';
|
|
124
|
+
import {Text, Button, View} from 'react-native';
|
|
125
|
+
import {useStore} from 'pocket-state';
|
|
126
|
+
import {counterStore} from './counterStore';
|
|
127
|
+
|
|
128
|
+
export function CounterComponent() {
|
|
129
|
+
const count = useStore(counterStore, s => s.count);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View>
|
|
133
|
+
<Text>Count: {count}</Text>
|
|
134
|
+
<Button
|
|
135
|
+
title="Inc"
|
|
136
|
+
onPress={() => counterStore.setValue(s => ({count: s.count + 1}))}
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
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.
|
package/package.json
CHANGED
package/src/README
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# Pocket State
|
|
2
|
-
|
|
3
|
-
A lightweight state management library for React, built on top of `useSyncExternalStore`.
|
|
4
|
-
Designed to be **tiny, fast, and predictable**
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Installation
|
|
9
|
-
|
|
10
|
-
Install with your favorite package manager:
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
# npm
|
|
14
|
-
npm install pocket-state
|
|
15
|
-
|
|
16
|
-
# yarn
|
|
17
|
-
yarn add pocket-state
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## API
|
|
24
|
-
|
|
25
|
-
### `createStore<T>(initialState: T, middlewares?: Middleware<T>[])`
|
|
26
|
-
|
|
27
|
-
Create a new store.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import {createStore} from 'pocket-state';
|
|
31
|
-
|
|
32
|
-
type Counter = {count: number};
|
|
33
|
-
|
|
34
|
-
const counterStore = createStore<Counter>({count: 0});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
### `useStore(store, selector?, equalityFn?)`
|
|
40
|
-
|
|
41
|
-
Subscribe to store state inside a React component.
|
|
42
|
-
It only re-renders when the selected slice changes.
|
|
43
|
-
|
|
44
|
-
```tsx
|
|
45
|
-
import {useStore} from 'pocket-state';
|
|
46
|
-
import {counterStore} from './counterStore';
|
|
47
|
-
|
|
48
|
-
function CounterDisplay() {
|
|
49
|
-
const count = useStore(counterStore, s => s.count);
|
|
50
|
-
return <Text>Count: {count}</Text>;
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
### `useShallowStore(store, selector)`
|
|
57
|
-
|
|
58
|
-
Like `useStore` but uses shallow equality. Useful when selecting an object.
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
import {useShallowStore} from 'pocket-state';
|
|
62
|
-
|
|
63
|
-
function Info() {
|
|
64
|
-
const {count, active} = useShallowStore(counterStore, s => ({
|
|
65
|
-
count: s.count,
|
|
66
|
-
active: s.active,
|
|
67
|
-
}));
|
|
68
|
-
return (
|
|
69
|
-
<Text>
|
|
70
|
-
{count} {active ? 'ON' : 'OFF'}
|
|
71
|
-
</Text>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
### Store API
|
|
79
|
-
|
|
80
|
-
Each store exposes:
|
|
81
|
-
|
|
82
|
-
- `getValue()` → current state
|
|
83
|
-
- `setValue(updater)` → update state (`updater` can be partial or producer)
|
|
84
|
-
- `reset(nextState?)` → reset to initial or custom state
|
|
85
|
-
- `subscribe(listener)` → subscribe to all changes
|
|
86
|
-
- `getNumberOfSubscriber()` → count active subscribers
|
|
87
|
-
|
|
88
|
-
Example:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
counterStore.setValue(s => ({count: s.count + 1}));
|
|
92
|
-
const state = counterStore.getValue();
|
|
93
|
-
console.log(state.count); // → 1
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Example
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
import React from 'react';
|
|
102
|
-
import {Text, Button, View} from 'react-native';
|
|
103
|
-
import {createStore, useStore} from 'pocket-state';
|
|
104
|
-
|
|
105
|
-
// Create store
|
|
106
|
-
const counterStore = createStore({count: 0});
|
|
107
|
-
|
|
108
|
-
export default function App() {
|
|
109
|
-
const count = useStore(counterStore, s => s.count);
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<View>
|
|
113
|
-
<Text>Count: {count}</Text>
|
|
114
|
-
<Button
|
|
115
|
-
title="Increment"
|
|
116
|
-
onPress={() => counterStore.setValue(s => ({count: s.count + 1}))}
|
|
117
|
-
/>
|
|
118
|
-
<Button title="Reset" onPress={() => counterStore.reset()} />
|
|
119
|
-
</View>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
🔥 That’s it — no boilerplate, no context providers.
|