pocket-state 0.0.3 → 0.0.5
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 +126 -0
- package/package.json +2 -2
- package/src/globalState/event.ts +9 -0
- package/src/globalState/hooks.ts +19 -47
- package/src/globalState/store.ts +18 -19
- package/src/globalState/type.d.ts +6 -0
package/README
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pocket-state",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "tiny global store",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"codegenConfig": {
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
},
|
|
11
11
|
"author": " <@kayda69> (nhh.tcp@gmail.com)",
|
|
12
12
|
"license": "ISC",
|
|
13
|
-
"homepage": "
|
|
13
|
+
"homepage": "README"
|
|
14
14
|
}
|
package/src/globalState/event.ts
CHANGED
|
@@ -78,4 +78,13 @@ export class EventEmitter implements IEventEmitter {
|
|
|
78
78
|
this.events.clear();
|
|
79
79
|
this.onceWrappers.clear();
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
getNumberOfSubscriber(event?: string): number {
|
|
83
|
+
if (event) {
|
|
84
|
+
return this.events.get(event)?.size ?? 0;
|
|
85
|
+
}
|
|
86
|
+
let total = 0;
|
|
87
|
+
this.events.forEach(set => (total += set.size));
|
|
88
|
+
return total;
|
|
89
|
+
}
|
|
81
90
|
}
|
package/src/globalState/hooks.ts
CHANGED
|
@@ -1,58 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
import {useCallback, useRef} from 'react';
|
|
3
|
-
import {useSyncExternalStore} from 'react';
|
|
1
|
+
import {useCallback, useRef, useSyncExternalStore} from 'react';
|
|
4
2
|
import type {Store} from './type';
|
|
5
|
-
import {shallow} from './shallowEqual';
|
|
6
|
-
|
|
7
|
-
export function useStore<T>(store: Store<T>): T;
|
|
8
|
-
export function useStore<T, S>(store: Store<T>, selector: (state: T) => S): S;
|
|
9
3
|
|
|
10
4
|
export function useStore<T, S = T>(
|
|
11
5
|
store: Store<T>,
|
|
12
6
|
selector?: (state: T) => S,
|
|
13
|
-
): T | S {
|
|
14
|
-
const subscribe = useCallback(
|
|
15
|
-
(onChange: () => void) =>
|
|
16
|
-
selector
|
|
17
|
-
? store.subscribe(selector, () => onChange())
|
|
18
|
-
: store.subscribe(() => onChange()),
|
|
19
|
-
[store, selector],
|
|
20
|
-
);
|
|
21
|
-
const getSnapshot = useCallback(() => {
|
|
22
|
-
const s = store.getValue();
|
|
23
|
-
return selector ? selector(s) : (s as T);
|
|
24
|
-
}, [store, selector]);
|
|
25
|
-
|
|
26
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) as T | S;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
//shallow
|
|
30
|
-
export function useShallowStore<T, S>(
|
|
31
|
-
store: Store<T>,
|
|
32
|
-
selector: (state: T) => S,
|
|
33
7
|
): S {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const lastSliceRef = useRef<S>(selector(store.getValue()));
|
|
8
|
+
const sliceRef = useRef<S>(
|
|
9
|
+
selector ? selector(store.getValue()) : (store.getValue() as unknown as S),
|
|
10
|
+
);
|
|
38
11
|
|
|
39
|
-
const subscribe = (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
12
|
+
const subscribe = useCallback(
|
|
13
|
+
(onChange: () => void) => {
|
|
14
|
+
if (selector) {
|
|
15
|
+
return store.subscribe(selector, (nextSlice: S) => {
|
|
16
|
+
sliceRef.current = nextSlice;
|
|
17
|
+
onChange();
|
|
18
|
+
});
|
|
45
19
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return lastSliceRef.current;
|
|
55
|
-
};
|
|
20
|
+
return store.subscribe((next: T) => {
|
|
21
|
+
sliceRef.current = next as unknown as S;
|
|
22
|
+
onChange();
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
[store, selector],
|
|
26
|
+
);
|
|
27
|
+
const getSnapshot = useCallback(() => sliceRef.current, []);
|
|
56
28
|
|
|
57
29
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
58
30
|
}
|
package/src/globalState/store.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
// store.ts
|
|
2
|
-
// import deepEqual from 'fast-deep-equal';
|
|
3
2
|
import {IEventEmitter, Listener, Middleware, Store, UseStoreGet} from './type';
|
|
4
3
|
import {EventEmitter} from './event';
|
|
5
4
|
import {Draft, produce} from 'immer';
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {shallow} from './shallowEqual';
|
|
6
|
+
|
|
8
7
|
export function createStore<T>(
|
|
9
8
|
initialState: T,
|
|
10
9
|
middlewares: Middleware<T>[] = [],
|
|
10
|
+
equalityFn?: (a: any, b: any) => boolean,
|
|
11
11
|
): Store<T> {
|
|
12
12
|
const emitter: IEventEmitter = new EventEmitter();
|
|
13
13
|
let state = initialState;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const areEqual = equalityFn ?? shallow;
|
|
16
|
+
|
|
16
17
|
let emitScheduled = false;
|
|
17
18
|
const emitState = () => {
|
|
18
19
|
if (emitScheduled) return;
|
|
@@ -25,13 +26,12 @@ export function createStore<T>(
|
|
|
25
26
|
|
|
26
27
|
function baseSet(delta: Partial<T>) {
|
|
27
28
|
const nextState = Array.isArray(state)
|
|
28
|
-
? (delta as unknown as T)
|
|
29
|
-
: {...state, ...delta};
|
|
29
|
+
? (delta as unknown as T)
|
|
30
|
+
: {...state, ...delta};
|
|
30
31
|
|
|
31
|
-
if (!
|
|
32
|
+
if (!areEqual(state, nextState)) {
|
|
32
33
|
state = nextState;
|
|
33
34
|
|
|
34
|
-
// Dev guard: phát hiện mutate ngoài store
|
|
35
35
|
if (process.env.NODE_ENV !== 'production') {
|
|
36
36
|
try {
|
|
37
37
|
Object.freeze(state as any);
|
|
@@ -61,22 +61,20 @@ export function createStore<T>(
|
|
|
61
61
|
let wrapped: Listener<any>;
|
|
62
62
|
|
|
63
63
|
if (typeof maybeListener === 'function') {
|
|
64
|
-
// subscribe(selector, listener)
|
|
65
64
|
const selector: (s: T) => any = selectorOrListener;
|
|
66
65
|
let prevSlice = selector(state);
|
|
67
66
|
wrapped = (next: T) => {
|
|
68
67
|
const slice = selector(next);
|
|
69
|
-
if (!
|
|
68
|
+
if (!areEqual(slice, prevSlice)) {
|
|
70
69
|
prevSlice = slice;
|
|
71
70
|
maybeListener(slice);
|
|
72
71
|
}
|
|
73
72
|
};
|
|
74
73
|
} else {
|
|
75
|
-
// subscribe(listener)
|
|
76
74
|
const listener: Listener<T> = selectorOrListener;
|
|
77
75
|
let prev = state;
|
|
78
76
|
wrapped = (next: T) => {
|
|
79
|
-
if (!
|
|
77
|
+
if (!areEqual(next, prev)) {
|
|
80
78
|
prev = next;
|
|
81
79
|
listener(next);
|
|
82
80
|
}
|
|
@@ -84,10 +82,6 @@ export function createStore<T>(
|
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
emitter.on('state', wrapped);
|
|
87
|
-
|
|
88
|
-
// Lưu ý: KHÔNG gọi listener ngay khi subscribe
|
|
89
|
-
// (để tương thích useSyncExternalStore: initial snapshot lấy qua getSnapshot)
|
|
90
|
-
|
|
91
85
|
return () => emitter.off('state', wrapped);
|
|
92
86
|
}
|
|
93
87
|
|
|
@@ -111,7 +105,7 @@ export function createStore<T>(
|
|
|
111
105
|
function setImmer(updater: (draft: Draft<T>) => void): void {
|
|
112
106
|
try {
|
|
113
107
|
const nextState = produce(state, updater);
|
|
114
|
-
if (
|
|
108
|
+
if (areEqual(state, nextState)) return;
|
|
115
109
|
|
|
116
110
|
if (Array.isArray(state)) {
|
|
117
111
|
setFn(nextState as unknown as Partial<T>);
|
|
@@ -168,7 +162,7 @@ export function createStore<T>(
|
|
|
168
162
|
}
|
|
169
163
|
}
|
|
170
164
|
const current = getValue();
|
|
171
|
-
if (!
|
|
165
|
+
if (!areEqual(current, next)) {
|
|
172
166
|
setFn(next as unknown as Partial<T>);
|
|
173
167
|
}
|
|
174
168
|
}
|
|
@@ -184,7 +178,11 @@ export function createStore<T>(
|
|
|
184
178
|
}
|
|
185
179
|
|
|
186
180
|
function isDirty() {
|
|
187
|
-
return !
|
|
181
|
+
return !areEqual(state, initialState);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getNumberOfSubscriber() {
|
|
185
|
+
return emitter.getNumberOfSubscriber();
|
|
188
186
|
}
|
|
189
187
|
return {
|
|
190
188
|
getValue,
|
|
@@ -194,5 +192,6 @@ export function createStore<T>(
|
|
|
194
192
|
reset,
|
|
195
193
|
subscribe,
|
|
196
194
|
isDirty,
|
|
195
|
+
getNumberOfSubscriber,
|
|
197
196
|
};
|
|
198
197
|
}
|
|
@@ -97,6 +97,9 @@ export interface IEventEmitter {
|
|
|
97
97
|
|
|
98
98
|
/** Remove all listeners for all events. */
|
|
99
99
|
clear(): void;
|
|
100
|
+
|
|
101
|
+
/** Get number of subscribe */
|
|
102
|
+
getNumberOfSubscriber(event?: string): number;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/**
|
|
@@ -194,6 +197,9 @@ export interface Store<T> {
|
|
|
194
197
|
* ```
|
|
195
198
|
*/
|
|
196
199
|
isDirty(): boolean;
|
|
200
|
+
|
|
201
|
+
/** Get number of subscriber for current store */
|
|
202
|
+
getNumberOfSubscriber(): number;
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
/**
|