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 CHANGED
@@ -106,27 +106,61 @@ const unsub = counterStore.subscribe(state => {
106
106
  console.log('New state:', state);
107
107
  });
108
108
 
109
- // With selector (push model)
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
- ### 6) Using with React
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 {useStore} from 'pocket-state';
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
- const count = useStore(counterStore, s => s.count);
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. It supports both full‑state and slice subscriptions.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pocket-state",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "tiny global store",
5
5
  "main": "src/index",
6
6
  "codegenConfig": {
@@ -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
+ }