pocket-state 0.1.2 → 0.1.4
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 +59 -168
- package/example/CounterExample.tsx +22 -0
- package/example/PressButtonExample.tsx +21 -0
- package/example/SelectorExample.tsx +22 -0
- package/example/SubscriptionExample.tsx +28 -0
- package/package.json +1 -1
- package/src/globalState/create.ts +41 -0
package/README.md
CHANGED
|
@@ -106,27 +106,61 @@ const unsub = counterStore.subscribe(state => {
|
|
|
106
106
|
console.log('New state:', state);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
// With selector
|
|
109
|
+
// With selector
|
|
110
110
|
const unsubCount = counterStore.subscribe(
|
|
111
111
|
s => s.count,
|
|
112
112
|
count => console.log('Count changed:', count),
|
|
113
113
|
);
|
|
114
114
|
|
|
115
|
-
// cleanup
|
|
116
115
|
unsub();
|
|
117
116
|
unsubCount();
|
|
118
117
|
```
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🎣 Custom Hooks with `createHook`
|
|
122
|
+
|
|
123
|
+
A utility to generate custom, type-safe hooks for your stores —
|
|
124
|
+
allowing you to access the store API without re-renders,
|
|
125
|
+
or subscribe to selected state slices with precise control.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import {createHook} from 'pocket-state';
|
|
129
|
+
import {counterStore} from './counterStore';
|
|
130
|
+
|
|
131
|
+
// Generate a custom hook
|
|
132
|
+
const useCounter = createHook(counterStore);
|
|
133
|
+
|
|
134
|
+
// Access only store API (no re-render)
|
|
135
|
+
const {reset, setValue} = useCounter();
|
|
136
|
+
|
|
137
|
+
// Access selected state (with API), triggers re-render on changes
|
|
138
|
+
const {value: count, reset} = useCounter(state => state.count);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 🧩 **React Example**
|
|
121
142
|
|
|
122
143
|
```tsx
|
|
123
144
|
import React from 'react';
|
|
124
145
|
import {Text, Button, View} from 'react-native';
|
|
125
|
-
import {
|
|
126
|
-
import {counterStore} from './counterStore';
|
|
146
|
+
import {createStore, createHook} from 'pocket-state';
|
|
127
147
|
|
|
148
|
+
// 1. Create your store
|
|
149
|
+
interface Counter {
|
|
150
|
+
count: number;
|
|
151
|
+
}
|
|
152
|
+
const counterStore = createStore<Counter>({count: 0});
|
|
153
|
+
|
|
154
|
+
// 2. Create the custom hook
|
|
155
|
+
const useCounter = createHook(counterStore);
|
|
156
|
+
|
|
157
|
+
// 3. Use inside a React Native component
|
|
128
158
|
export function CounterComponent() {
|
|
129
|
-
|
|
159
|
+
// Only gets API, no re-render
|
|
160
|
+
const {reset} = useCounter();
|
|
161
|
+
|
|
162
|
+
// Gets selected value + API, re-renders when count changes
|
|
163
|
+
const {value: count, reset: reset2} = useCounter(state => state.count);
|
|
130
164
|
|
|
131
165
|
return (
|
|
132
166
|
<View>
|
|
@@ -135,19 +169,24 @@ export function CounterComponent() {
|
|
|
135
169
|
title="Inc"
|
|
136
170
|
onPress={() => counterStore.setValue(s => ({count: s.count + 1}))}
|
|
137
171
|
/>
|
|
172
|
+
<Button title="Reset" onPress={reset} />
|
|
138
173
|
</View>
|
|
139
174
|
);
|
|
140
175
|
}
|
|
141
176
|
```
|
|
142
177
|
|
|
178
|
+
**Advantages:**
|
|
179
|
+
|
|
180
|
+
- **No accidental re-renders** when accessing only API methods.
|
|
181
|
+
- **Type-safe selectors** and API usage, with full IDE support.
|
|
182
|
+
- **Simple migration path** for those coming from Zustand or similar libraries.
|
|
183
|
+
|
|
143
184
|
---
|
|
144
185
|
|
|
145
186
|
## 🎯 Selectors
|
|
146
187
|
|
|
147
188
|
Selectors let you subscribe to **just part of the state**.
|
|
148
189
|
|
|
149
|
-
### React
|
|
150
|
-
|
|
151
190
|
```tsx
|
|
152
191
|
function FlagDisplay() {
|
|
153
192
|
const flag = useStore(counterStore, s => s.flag);
|
|
@@ -155,165 +194,6 @@ function FlagDisplay() {
|
|
|
155
194
|
}
|
|
156
195
|
```
|
|
157
196
|
|
|
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
197
|
---
|
|
318
198
|
|
|
319
199
|
## 🧩 API Reference
|
|
@@ -331,10 +211,21 @@ const c = counterStore.getValue('count'); // typed as number
|
|
|
331
211
|
|
|
332
212
|
### `useStore(store, selector?)` (React)
|
|
333
213
|
|
|
334
|
-
Lightweight hook built on `useSyncExternalStore` that subscribes to your store and returns the selected slice.
|
|
214
|
+
Lightweight hook built on `useSyncExternalStore` that subscribes to your store and returns the selected slice.
|
|
215
|
+
|
|
216
|
+
### `createHook(store)` (React)
|
|
217
|
+
|
|
218
|
+
Generates a custom hook for your store.
|
|
219
|
+
|
|
220
|
+
- `hook()` → store API only, **no re-render**.
|
|
221
|
+
- `hook(selector)` → `{ value, ...api }`, re-renders when selected state changes.
|
|
335
222
|
|
|
336
223
|
---
|
|
337
224
|
|
|
225
|
+
## keyword
|
|
226
|
+
|
|
227
|
+
pocket-state, state-management, react, react-native, typescript, hooks, store
|
|
228
|
+
|
|
338
229
|
## 📜 License
|
|
339
230
|
|
|
340
231
|
MIT — use it however you like.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Text, Button, View} from 'react-native';
|
|
3
|
+
import {createStore, createHook} from 'pocket-state';
|
|
4
|
+
|
|
5
|
+
interface Counter {
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
const counterStore = createStore<Counter>({ count: 0 });
|
|
9
|
+
const useCounter = createHook(counterStore);
|
|
10
|
+
|
|
11
|
+
export function CounterExample() {
|
|
12
|
+
const {reset} = useCounter();
|
|
13
|
+
const {value: count} = useCounter(state => state.count);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View style={{padding: 20}}>
|
|
17
|
+
<Text>Count: {count}</Text>
|
|
18
|
+
<Button title="Inc" onPress={() => counterStore.setValue(s => ({count: s.count + 1}))} />
|
|
19
|
+
<Button title="Reset" onPress={reset} />
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Text, Button, View} from 'react-native';
|
|
3
|
+
import {createStore, createHook} from 'pocket-state';
|
|
4
|
+
|
|
5
|
+
interface Press {
|
|
6
|
+
press: number;
|
|
7
|
+
}
|
|
8
|
+
const pressStore = createStore<Press>({ press: 0 });
|
|
9
|
+
const usePress = createHook(pressStore);
|
|
10
|
+
|
|
11
|
+
export function PressButtonExample() {
|
|
12
|
+
const {value: press} = usePress(state => state.press);
|
|
13
|
+
const {setValue} = usePress();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View style={{padding: 20}}>
|
|
17
|
+
<Text>Press count: {press}</Text>
|
|
18
|
+
<Button title="Press +" onPress={() => setValue(s => ({press: s.press + 1}))} />
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Text, View} from 'react-native';
|
|
3
|
+
import {createStore, createHook} from 'pocket-state';
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
name: string;
|
|
7
|
+
age: number;
|
|
8
|
+
}
|
|
9
|
+
const userStore = createStore<User>({ name: 'Alice', age: 25 });
|
|
10
|
+
const useUser = createHook(userStore);
|
|
11
|
+
|
|
12
|
+
export function SelectorExample() {
|
|
13
|
+
const {value: name} = useUser(s => s.name);
|
|
14
|
+
const {value: age} = useUser(s => s.age);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View style={{padding: 20}}>
|
|
18
|
+
<Text>User name: {name}</Text>
|
|
19
|
+
<Text>User age: {age}</Text>
|
|
20
|
+
</View>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import {Text, View} from 'react-native';
|
|
3
|
+
import {createStore, createHook} from 'pocket-state';
|
|
4
|
+
|
|
5
|
+
interface Log {
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
const logStore = createStore<Log>({ count: 0 });
|
|
9
|
+
const useLog = createHook(logStore);
|
|
10
|
+
|
|
11
|
+
export function SubscriptionExample() {
|
|
12
|
+
const {setValue, subscribe} = useLog();
|
|
13
|
+
const {value: count} = useLog(s => s.count);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const unsub = subscribe(s => s.count, (c) => {
|
|
17
|
+
console.log('Count changed:', c);
|
|
18
|
+
});
|
|
19
|
+
return unsub;
|
|
20
|
+
}, [subscribe]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={{padding: 20}}>
|
|
24
|
+
<Text>Count: {count}</Text>
|
|
25
|
+
<Text>(Open console to see subscription logs)</Text>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useStore } from "./hooks";
|
|
3
|
+
import { Store } from "./type";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a custom React hook for a store, giving you access to the store API
|
|
7
|
+
* (such as reset, set, subscribe, etc.) and optionally, reactive state selection.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* // 1. Create your store instance (with custom API)
|
|
11
|
+
* const countStore = createStore({ count: 0, ... });
|
|
12
|
+
*
|
|
13
|
+
* // 2. Create the hook
|
|
14
|
+
* const useCount = createHook(countStore);
|
|
15
|
+
*
|
|
16
|
+
* // 3. Use inside component:
|
|
17
|
+
* // a) Access API only, never triggers re-render
|
|
18
|
+
* const { reset, setValue } = useCount();
|
|
19
|
+
*
|
|
20
|
+
* // b) Access selected value (and API), triggers re-render only when value changes
|
|
21
|
+
* const { value, reset } = useCount(state => state.count);
|
|
22
|
+
*
|
|
23
|
+
* @template T The store state type.
|
|
24
|
+
* @param store - The store instance implementing the Store<T> API.
|
|
25
|
+
* @returns A custom hook with two usage patterns:
|
|
26
|
+
* 1. `useHook()` – Returns store API only (no value, no render on state change).
|
|
27
|
+
* 2. `useHook(selector)` – Returns `{ value, ...api }` where `value` is the selected state. Re-renders on selected value changes.
|
|
28
|
+
*/
|
|
29
|
+
export function createHook<T>(store: Store<T>) {
|
|
30
|
+
const api = { ...store };
|
|
31
|
+
|
|
32
|
+
function useBoundStore(): typeof api;
|
|
33
|
+
function useBoundStore<S>(selector: (state: T) => S): { value: S } & typeof api;
|
|
34
|
+
function useBoundStore<S = T>(selector?: (state: T) => S) {
|
|
35
|
+
if (!selector) return api;
|
|
36
|
+
const value = useStore(store, selector);
|
|
37
|
+
return useMemo(() => ({ value, ...api }), [value]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return useBoundStore;
|
|
41
|
+
}
|