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.
Files changed (3) hide show
  1. package/README.md +340 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pocket-state",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "tiny global store",
5
5
  "main": "src/index",
6
6
  "codegenConfig": {
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.