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.
Files changed (2) hide show
  1. package/README.md +281 -67
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,121 +1,140 @@
1
- # Pocket State
1
+ # pocket-state
2
2
 
3
- A lightweight state management library for React, built on top of `useSyncExternalStore`.
4
- Designed to be **tiny, fast, and predictable**
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
- ## Installation
9
+ ## ✨ Features
9
10
 
10
- Install with your favorite package manager:
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
- ## API
24
-
25
- ### `createStore<T>(initialState: T, middlewares?: Middleware<T>[])`
33
+ ## 🚀 Usage
26
34
 
27
- Create a new store.
35
+ ### 1) Create a Store
28
36
 
29
37
  ```ts
30
38
  import {createStore} from 'pocket-state';
31
39
 
32
- type Counter = {count: number};
40
+ interface Counter {
41
+ count: number;
42
+ flag: boolean;
43
+ }
33
44
 
34
- const counterStore = createStore<Counter>({count: 0});
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
- ### `useStore(store, selector?, equalityFn?)`
53
+ ```ts
54
+ // Read full state
55
+ console.log(counterStore.getValue()); // { count: 0, flag: false }
40
56
 
41
- Subscribe to store state inside a React component.
42
- It only re-renders when the selected slice changes.
57
+ // Read by key
58
+ console.log(counterStore.getValue('count')); // 0
43
59
 
44
- ```tsx
45
- import {useStore} from 'pocket-state';
46
- import {counterStore} from './counterStore';
60
+ // Update via partial
61
+ counterStore.setValue({flag: true});
47
62
 
48
- function CounterDisplay() {
49
- const count = useStore(counterStore, s => s.count);
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
- ### `useShallowStore(store, selector)`
73
+ ### 3) Immer Updates
57
74
 
58
- Like `useStore` but uses shallow equality. Useful when selecting an object.
75
+ 📦 Install immer
59
76
 
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
- }
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
- ### Store API
92
+ ### 4) Reset & Dirty Check
79
93
 
80
- Each store exposes:
94
+ ```ts
95
+ counterStore.reset(); // reset to initial
96
+ counterStore.reset({count: 10}); // reset with override
81
97
 
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
98
+ console.log(counterStore.isDirty()); // true/false
99
+ ```
87
100
 
88
- Example:
101
+ ### 5) Subscriptions (Outside React)
89
102
 
90
103
  ```ts
91
- counterStore.setValue(s => ({count: s.count + 1}));
92
- const state = counterStore.getValue();
93
- console.log(state.count); // → 1
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 {createStore, useStore} from 'pocket-state';
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 default function App() {
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="Increment"
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
- 🔥 That’s it — no boilerplate, no context providers.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pocket-state",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "tiny global store",
5
5
  "main": "src/index",
6
6
  "codegenConfig": {