@t8/react-store 1.1.3 → 1.2.0
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 +10 -10
- package/dist/index.js +83 -61
- package/package.json +2 -2
- package/src/useStore.ts +3 -6
package/README.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@t8/react-store)   
|
|
6
6
|
|
|
7
|
+
**Why?** To fill the gap with a state management lib that at the same time: (1) has a simple API introducing as few new entities as possible, (2) closely follows the React's `useState()` pattern of initializing and manipulating the state to fulfill the common task of migration from local state to shared state quickly without sizable rewrites, (3) doesn't internally make use of global stores or other global variables by default and works smoothly with SSR with regular React Contexts.
|
|
8
|
+
|
|
9
|
+
**Features**: `useState()`-like API for shared state, quick transition from local state, no boilerplate, easily integrates with Immer, SSR- and CSR-compatible.
|
|
10
|
+
|
|
11
|
+
<!-- docsgen-show-start --
|
|
7
12
|
```diff
|
|
8
13
|
+ let store = new Store(0);
|
|
9
14
|
|
|
@@ -18,12 +23,7 @@
|
|
|
18
23
|
return <button onClick={handleClick}>+ {counter}</button>;
|
|
19
24
|
};
|
|
20
25
|
```
|
|
21
|
-
|
|
22
|
-
- Similar to `useState()`
|
|
23
|
-
- Quick transition from local state
|
|
24
|
-
- No boilerplate
|
|
25
|
-
- Easily integrates with Immer
|
|
26
|
-
- SSR- and CSR-compatible
|
|
26
|
+
-- docsgen-show-end -->
|
|
27
27
|
|
|
28
28
|
Installation: `npm i @t8/react-store`
|
|
29
29
|
|
|
@@ -171,16 +171,16 @@ Immer can be used with `useStore()` just the same way as [with `useState()`](htt
|
|
|
171
171
|
|
|
172
172
|
The ready-to-use hook from the [T8 React Pending](https://github.com/t8js/react-pending) package helps manage shared async action state without disturbing the app's state management and actions' code.
|
|
173
173
|
|
|
174
|
-
##
|
|
174
|
+
## Persistence across remounts
|
|
175
175
|
|
|
176
176
|
A standalone store initialized outside a component can be used by the component as remount-persistent state, whether used by other components or not.
|
|
177
177
|
|
|
178
178
|
## Persistence across page reloads
|
|
179
179
|
|
|
180
180
|
```js
|
|
181
|
-
import {
|
|
181
|
+
import { PersistentStore } from "@t8/react-store";
|
|
182
182
|
|
|
183
|
-
let counterStore =
|
|
183
|
+
let counterStore = new PersistentStore(0, "counter");
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
Whenever it's updated, `counterStore` above will save its state to the `"counter"` key of `localStorage`. (Pass `true` as the third parameter of `
|
|
186
|
+
Whenever it's updated, `counterStore` above will save its state to the `"counter"` key of `localStorage`. (Pass `true` as the third parameter of `new PersistentStore()` to use `sessionStorage` instead of `localStorage`.) Also, the store's initial state value is restored from the browser storage. Otherwise, `counterStore` works pretty much like a regular store described above.
|
package/dist/index.js
CHANGED
|
@@ -1,76 +1,27 @@
|
|
|
1
1
|
// node_modules/@t8/store/src/isStore.ts
|
|
2
2
|
function isStore(x) {
|
|
3
|
-
return x !== null && typeof x === "object" && "
|
|
3
|
+
return x !== null && typeof x === "object" && "onUpdate" in x && typeof x.onUpdate === "function" && "getState" in x && typeof x.getState === "function" && "setState" in x && typeof x.setState === "function";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
// node_modules/@t8/store/src/
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
return session ? sessionStorage : localStorage;
|
|
10
|
-
}
|
|
11
|
-
function persist(store, storageKey, session = false) {
|
|
12
|
-
let inited = false;
|
|
13
|
-
function read(state) {
|
|
14
|
-
let storage = getStorage(session);
|
|
15
|
-
let rawState = null;
|
|
16
|
-
if (storage) {
|
|
17
|
-
try {
|
|
18
|
-
rawState = storage.getItem(storageKey);
|
|
19
|
-
if (rawState !== null) store.setState(JSON.parse(rawState));
|
|
20
|
-
} catch {
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (!inited) {
|
|
24
|
-
inited = true;
|
|
25
|
-
if (rawState === null) write(state);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function write(state) {
|
|
29
|
-
let storage = getStorage(session);
|
|
30
|
-
if (inited && storage) {
|
|
31
|
-
try {
|
|
32
|
-
storage.setItem(storageKey, JSON.stringify(state));
|
|
33
|
-
} catch {
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
store.on("sync", read);
|
|
38
|
-
store.once("effect", read);
|
|
39
|
-
store.on("update", write);
|
|
40
|
-
return store;
|
|
6
|
+
// node_modules/@t8/store/src/isPersistentStore.ts
|
|
7
|
+
function isPersistentStore(x) {
|
|
8
|
+
return isStore(x) && "sync" in x;
|
|
41
9
|
}
|
|
42
10
|
|
|
43
11
|
// node_modules/@t8/store/src/Store.ts
|
|
44
12
|
var Store = class {
|
|
45
13
|
state;
|
|
46
|
-
callbacks =
|
|
14
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
47
15
|
revision = -1;
|
|
48
16
|
constructor(data) {
|
|
49
17
|
this.state = data;
|
|
50
18
|
}
|
|
51
|
-
|
|
52
|
-
|
|
19
|
+
onUpdate(callback) {
|
|
20
|
+
this.callbacks.add(callback);
|
|
53
21
|
return () => {
|
|
54
|
-
this.
|
|
22
|
+
this.callbacks.delete(callback);
|
|
55
23
|
};
|
|
56
24
|
}
|
|
57
|
-
off(event, callback) {
|
|
58
|
-
this.callbacks[event]?.delete(callback);
|
|
59
|
-
}
|
|
60
|
-
once(event, callback) {
|
|
61
|
-
let oneTimeCallback = (nextState, prevState) => {
|
|
62
|
-
this.off(event, oneTimeCallback);
|
|
63
|
-
callback(nextState, prevState);
|
|
64
|
-
};
|
|
65
|
-
return this.on(event, oneTimeCallback);
|
|
66
|
-
}
|
|
67
|
-
emit(event, nextState, prevState) {
|
|
68
|
-
let eventCallbacks = this.callbacks[event];
|
|
69
|
-
if (eventCallbacks) {
|
|
70
|
-
for (let callback of eventCallbacks)
|
|
71
|
-
callback(nextState ?? this.state, prevState ?? this.state);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
25
|
getState() {
|
|
75
26
|
return this.state;
|
|
76
27
|
}
|
|
@@ -79,7 +30,77 @@ var Store = class {
|
|
|
79
30
|
let nextState = update instanceof Function ? update(this.state) : update;
|
|
80
31
|
this.state = nextState;
|
|
81
32
|
this.revision = Math.random();
|
|
82
|
-
this.
|
|
33
|
+
for (let callback of this.callbacks) callback(nextState, prevState);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// node_modules/@t8/store/src/PersistentStore.ts
|
|
38
|
+
function getStorage(session) {
|
|
39
|
+
if (typeof window === "undefined") return;
|
|
40
|
+
return session ? sessionStorage : localStorage;
|
|
41
|
+
}
|
|
42
|
+
var PersistentStore = class extends Store {
|
|
43
|
+
storageKey;
|
|
44
|
+
session;
|
|
45
|
+
synced = false;
|
|
46
|
+
/**
|
|
47
|
+
* Creates an instance of the container for data persistent across page
|
|
48
|
+
* reloads.
|
|
49
|
+
*
|
|
50
|
+
* The store data is saved to and restored from the given `storageKey`
|
|
51
|
+
* either of `localStorage` if the `session` parameter is `false` (which is
|
|
52
|
+
* the default), or of `sessionStorage` if `session` is set to `true`.
|
|
53
|
+
* Interaction with the browser storage is skipped in non-browser environments.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```js
|
|
57
|
+
* let counterStore = new PersistentStore(0, "counter");
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
constructor(data, storageKey, session = false) {
|
|
61
|
+
super(data);
|
|
62
|
+
this.storageKey = storageKey;
|
|
63
|
+
this.session = session;
|
|
64
|
+
this.onUpdate(() => {
|
|
65
|
+
if (this.synced) this.save();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Saves the store state value to the browser storage.
|
|
70
|
+
*/
|
|
71
|
+
save() {
|
|
72
|
+
let storage = getStorage(this.session);
|
|
73
|
+
if (this.synced && storage) {
|
|
74
|
+
try {
|
|
75
|
+
storage.setItem(this.storageKey, JSON.stringify(this.state));
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Signals the store to read the state value from the browser storage.
|
|
82
|
+
*/
|
|
83
|
+
sync() {
|
|
84
|
+
let storage = getStorage(this.session);
|
|
85
|
+
let rawState = null;
|
|
86
|
+
if (storage) {
|
|
87
|
+
try {
|
|
88
|
+
rawState = storage.getItem(this.storageKey);
|
|
89
|
+
if (rawState !== null) this.setState(JSON.parse(rawState));
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!this.synced) {
|
|
94
|
+
this.synced = true;
|
|
95
|
+
if (rawState === null) this.save();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Signals the store to read the state value from the browser storage once,
|
|
100
|
+
* disregarding subsequest `syncOnce()` calls.
|
|
101
|
+
*/
|
|
102
|
+
syncOnce() {
|
|
103
|
+
if (!this.synced) this.sync();
|
|
83
104
|
}
|
|
84
105
|
};
|
|
85
106
|
|
|
@@ -92,9 +113,9 @@ function useStore(store, shouldUpdate = true) {
|
|
|
92
113
|
let setState = useMemo(() => store.setState.bind(store), [store]);
|
|
93
114
|
let initialStoreRevision = useRef(store.revision);
|
|
94
115
|
useEffect(() => {
|
|
95
|
-
store.
|
|
116
|
+
if (isPersistentStore(store)) store.syncOnce();
|
|
96
117
|
if (!shouldUpdate) return;
|
|
97
|
-
let unsubscribe = store.
|
|
118
|
+
let unsubscribe = store.onUpdate((nextState, prevState) => {
|
|
98
119
|
if (typeof shouldUpdate !== "function" || shouldUpdate(nextState, prevState))
|
|
99
120
|
setRevision(Math.random());
|
|
100
121
|
});
|
|
@@ -108,8 +129,9 @@ function useStore(store, shouldUpdate = true) {
|
|
|
108
129
|
return [state, setState];
|
|
109
130
|
}
|
|
110
131
|
export {
|
|
132
|
+
PersistentStore,
|
|
111
133
|
Store,
|
|
134
|
+
isPersistentStore,
|
|
112
135
|
isStore,
|
|
113
|
-
persist,
|
|
114
136
|
useStore
|
|
115
137
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t8/react-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Small React app state management lib aligned with React's state pattern, condensed to the essentials",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"typescript": "^5.9.3"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@t8/store": "^1.2
|
|
48
|
+
"@t8/store": "^1.3.2"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/useStore.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isStore, type Store } from "@t8/store";
|
|
1
|
+
import { isPersistentStore, isStore, type Store } from "@t8/store";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
export type SetStoreState<T> = Store<T>["setState"];
|
|
@@ -45,14 +45,11 @@ export function useStore<T>(
|
|
|
45
45
|
let initialStoreRevision = useRef(store.revision);
|
|
46
46
|
|
|
47
47
|
useEffect(() => {
|
|
48
|
-
|
|
49
|
-
// initialize the store state on the client without causing a
|
|
50
|
-
// hydration error.
|
|
51
|
-
store.emit("effect");
|
|
48
|
+
if (isPersistentStore<T>(store)) store.syncOnce();
|
|
52
49
|
|
|
53
50
|
if (!shouldUpdate) return;
|
|
54
51
|
|
|
55
|
-
let unsubscribe = store.
|
|
52
|
+
let unsubscribe = store.onUpdate((nextState, prevState) => {
|
|
56
53
|
if (
|
|
57
54
|
typeof shouldUpdate !== "function" ||
|
|
58
55
|
shouldUpdate(nextState, prevState)
|