@t8/react-store 1.2.6 → 1.2.8
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 +19 -21
- package/dist/index.cjs +11 -11
- package/dist/index.d.ts +10 -10
- package/dist/index.mjs +11 -11
- package/package.json +8 -8
- package/src/useStore.ts +14 -14
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# T8 React Store
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React app state management condensed to the essentials
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@t8/react-store) 
|
|
5
|
+
[](https://www.npmjs.com/package/@t8/react-store) 
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
This picture is achieved here by (1) having a simple API introducing as few new entities as possible, (2) closely following the React's `useState()` pattern of initializing and manipulating the state to avoid boilerplate and sizable rewrites in the common task of migration from local state to shared state, (3) working smoothly with SSR with regular React Contexts without requiring a specifically designed setup and without internally making use of global stores or other global variables by default.
|
|
7
|
+
**Features:** Quickest migration from local state · Familiar useState pattern · CSR/SSR without workarounds · Integrable with Immer · Quickly pluggable persistence across page reloads
|
|
10
8
|
|
|
11
9
|
<!-- docsgen-show-start --
|
|
12
10
|
```diff
|
|
@@ -29,7 +27,7 @@ Installation: `npm i @t8/react-store`
|
|
|
29
27
|
|
|
30
28
|
## Shared state setup
|
|
31
29
|
|
|
32
|
-
Moving
|
|
30
|
+
Moving local state to the full-fledged shared state:
|
|
33
31
|
|
|
34
32
|
```diff
|
|
35
33
|
+ import { Store, useStore } from "@t8/react-store";
|
|
@@ -64,29 +62,29 @@ Moving the local state to the full-fledged shared state:
|
|
|
64
62
|
[Live counter demo](https://codesandbox.io/p/sandbox/szhdnw?file=%252Fsrc%252FApp.tsx)<br>
|
|
65
63
|
[Tic-tac-toe](https://codesandbox.io/p/sandbox/tq852v?file=%252Fsrc%252FApp.tsx)
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
⬥ The shared state setup shown above is very similar to `useState()` allowing for quick migration from local state to shared state or the other way around.
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
⬥ The optional `false` parameter in `useStore(store, false)` (as in `<ResetButton>` above) tells the hook not to subscribe the component to tracking the store updates. The common use case is when a component makes use of the store value setter without using the store value.
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
⬥ With SSR, it's common practice to put shared values into React Context rather than module-level variables to avoid cross-request data sharing. The same applies to stores, see the examples in the [Sharing state via Context](#sharing-state-via-context) section below.
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
⬥ Similarly to instances of the built-in data container classes, such as `Set` and `Map`, stores are created as `new Store(data)` rather than with a factory function.
|
|
74
72
|
|
|
75
73
|
## Single store or multiple stores
|
|
76
74
|
|
|
77
75
|
An application can have as many stores as needed.
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
⬥ Splitting data into multiple stores is one of the strategies to maintain more targeted subscriptions to data changes in components. The other strategy is filtering store updates at the component level, which is discussed below.
|
|
80
78
|
|
|
81
79
|
## Filtering store updates
|
|
82
80
|
|
|
83
|
-
When only the store
|
|
81
|
+
When only the store value setter is required, without the store value, we can opt out from subscription to store changes by passing `false` as the parameter of `useStore()`:
|
|
84
82
|
|
|
85
83
|
```js
|
|
86
|
-
let [,
|
|
84
|
+
let [, setValue] = useStore(store, false);
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
Apart from a boolean, `useStore(store, shouldUpdate)` accepts a function of `(
|
|
87
|
+
Apart from a boolean, `useStore(store, shouldUpdate)` accepts a function of `(nextValue, prevValue) => boolean` as the optional second parameter to filter store value updates to respond to:
|
|
90
88
|
|
|
91
89
|
```jsx
|
|
92
90
|
import { useStore } from "@t8/react-store";
|
|
@@ -134,7 +132,7 @@ Shared state can be provided to the app by means of a regular React Context prov
|
|
|
134
132
|
|
|
135
133
|
[Live counter demo with Context](https://codesandbox.io/p/sandbox/rtng37?file=%2Fsrc%2FPlusButton.jsx)
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
⬥ In a multi-store setup, stores can be located in a single Context or split across multiple Contexts, just like any application data.
|
|
138
136
|
|
|
139
137
|
```jsx
|
|
140
138
|
import { createContext, useContext } from "react";
|
|
@@ -153,11 +151,11 @@ let ItemCard = ({ id }) => {
|
|
|
153
151
|
};
|
|
154
152
|
```
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
⬥ Note that updating the store value doesn't change the store reference sitting in the React Context and therefore doesn't cause updates of the entire Context. Only the components subscribed to updates in the particular store by means of `useStore(store)` will be notified to re-render.
|
|
157
155
|
|
|
158
156
|
## Store data
|
|
159
157
|
|
|
160
|
-
A store can contain data of any
|
|
158
|
+
A store can contain data of any kind, whether of a primitive type or nonprimitive.
|
|
161
159
|
|
|
162
160
|
Live demos:<br>
|
|
163
161
|
[Primitive value state](https://codesandbox.io/p/sandbox/rtng37?file=%2Fsrc%2FPlusButton.jsx)<br>
|
|
@@ -171,7 +169,7 @@ Immer can be used with `useStore()` just the same way as [with `useState()`](htt
|
|
|
171
169
|
|
|
172
170
|
## Shared loading state
|
|
173
171
|
|
|
174
|
-
The ready-to-use hook from
|
|
172
|
+
The ready-to-use hook from [React Pending](https://github.com/t8js/react-pending) helps manage shared async action state without rearranging the app's state management and actions' code.
|
|
175
173
|
|
|
176
174
|
## Persistence across remounts
|
|
177
175
|
|
|
@@ -179,7 +177,7 @@ A standalone store initialized outside a component can be used by the component
|
|
|
179
177
|
|
|
180
178
|
## Persistence across page reloads
|
|
181
179
|
|
|
182
|
-
Replacing `new Store(data)` with `new PersistentStore(data, storageKey)` as shown below gets the store's
|
|
180
|
+
Replacing `new Store(data)` with `new PersistentStore(data, storageKey)` as shown below gets the store's value initially restored from and saved whenever updated to `storageKey` in `localStorage`. (Pass `{ session: true }` as the `options` parameter of `new PersistentStore(data, storageKey, options?)` to use `sessionStorage` instead of `localStorage`.) Otherwise, persistent stores work pretty much like regular stores described above.
|
|
183
181
|
|
|
184
182
|
```js
|
|
185
183
|
import { PersistentStore } from "@t8/react-store";
|
|
@@ -187,6 +185,6 @@ import { PersistentStore } from "@t8/react-store";
|
|
|
187
185
|
let counterStore = new PersistentStore(0, "counter");
|
|
188
186
|
```
|
|
189
187
|
|
|
190
|
-
|
|
188
|
+
⬥ The way data gets saved to and restored from a browser storage entry (including filtering out certain data or otherwise rearranging the saved data) can be redefined by setting `options.serialize` and `options.deserialize` in `new PersistentStore(data, storageKey, options?)`. By default, these options act like `JSON.stringify()` and `JSON.parse()` respectively.
|
|
191
189
|
|
|
192
|
-
|
|
190
|
+
⬥ `PersistentStore` skips interaction with the browser storage in non-browser environments, which makes it equally usable with SSR.
|
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,8 @@ let __t8_store = require("@t8/store");
|
|
|
2
2
|
let react = require("react");
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Returns the
|
|
6
|
-
*
|
|
5
|
+
* Returns the value of `store` passed as the parameter and a
|
|
6
|
+
* function to update the store state value.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```js
|
|
@@ -16,29 +16,29 @@ let react = require("react");
|
|
|
16
16
|
*
|
|
17
17
|
* `shouldUpdate` can be set to `false` to prevent subscription
|
|
18
18
|
* to the store updates. Use case: if the component only requires
|
|
19
|
-
* the store
|
|
20
|
-
*
|
|
19
|
+
* the store value setter but not the store value, the component
|
|
20
|
+
* may not need to respond to the store updates at all:
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
23
23
|
* ```js
|
|
24
24
|
* let [, setValue] = useStore(store, false);
|
|
25
25
|
* ```
|
|
26
26
|
*
|
|
27
|
-
* `shouldUpdate` can also be a function `(
|
|
28
|
-
* to make the component respond only to specific store
|
|
27
|
+
* `shouldUpdate` can also be a function `(nextValue, prevValue) => boolean`
|
|
28
|
+
* to make the component respond only to specific store value changes,
|
|
29
29
|
* when this function returns `true`.
|
|
30
30
|
*/
|
|
31
31
|
function useStore(store, shouldUpdate = true) {
|
|
32
32
|
if (!(0, __t8_store.isStore)(store)) throw new Error("'store' is not an instance of Store");
|
|
33
33
|
let [, setRevision] = (0, react.useState)(-1);
|
|
34
|
-
let
|
|
35
|
-
let
|
|
34
|
+
let value = store.getValue();
|
|
35
|
+
let setValue = (0, react.useMemo)(() => store.setValue.bind(store), [store]);
|
|
36
36
|
let initialStoreRevision = (0, react.useRef)(store.revision);
|
|
37
37
|
(0, react.useEffect)(() => {
|
|
38
38
|
if ((0, __t8_store.isPersistentStore)(store)) store.syncOnce();
|
|
39
39
|
if (!shouldUpdate) return;
|
|
40
|
-
let unsubscribe = store.onUpdate((
|
|
41
|
-
if (typeof shouldUpdate !== "function" || shouldUpdate(
|
|
40
|
+
let unsubscribe = store.onUpdate((nextValue, prevValue) => {
|
|
41
|
+
if (typeof shouldUpdate !== "function" || shouldUpdate(nextValue, prevValue)) setRevision(Math.random());
|
|
42
42
|
});
|
|
43
43
|
if (store.revision !== initialStoreRevision.current) setRevision(Math.random());
|
|
44
44
|
return () => {
|
|
@@ -46,7 +46,7 @@ function useStore(store, shouldUpdate = true) {
|
|
|
46
46
|
initialStoreRevision.current = store.revision;
|
|
47
47
|
};
|
|
48
48
|
}, [store, shouldUpdate]);
|
|
49
|
-
return [
|
|
49
|
+
return [value, setValue];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
exports.useStore = useStore;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Store } from "@t8/store";
|
|
2
2
|
export * from "@t8/store";
|
|
3
3
|
|
|
4
|
-
type
|
|
5
|
-
type ShouldUpdateCallback<T> = (
|
|
4
|
+
type SetStoreValue<T> = Store<T>["setValue"];
|
|
5
|
+
type ShouldUpdateCallback<T> = (nextValue: T, prevValue: T) => boolean;
|
|
6
6
|
type ShouldUpdate<T> = boolean | ShouldUpdateCallback<T>;
|
|
7
7
|
/**
|
|
8
|
-
* Returns the
|
|
9
|
-
*
|
|
8
|
+
* Returns the value of `store` passed as the parameter and a
|
|
9
|
+
* function to update the store state value.
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```js
|
|
@@ -19,18 +19,18 @@ type ShouldUpdate<T> = boolean | ShouldUpdateCallback<T>;
|
|
|
19
19
|
*
|
|
20
20
|
* `shouldUpdate` can be set to `false` to prevent subscription
|
|
21
21
|
* to the store updates. Use case: if the component only requires
|
|
22
|
-
* the store
|
|
23
|
-
*
|
|
22
|
+
* the store value setter but not the store value, the component
|
|
23
|
+
* may not need to respond to the store updates at all:
|
|
24
24
|
*
|
|
25
25
|
* @example
|
|
26
26
|
* ```js
|
|
27
27
|
* let [, setValue] = useStore(store, false);
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* `shouldUpdate` can also be a function `(
|
|
31
|
-
* to make the component respond only to specific store
|
|
30
|
+
* `shouldUpdate` can also be a function `(nextValue, prevValue) => boolean`
|
|
31
|
+
* to make the component respond only to specific store value changes,
|
|
32
32
|
* when this function returns `true`.
|
|
33
33
|
*/
|
|
34
|
-
declare function useStore<T>(store: Store<T>, shouldUpdate?: ShouldUpdate<T>): [T,
|
|
34
|
+
declare function useStore<T>(store: Store<T>, shouldUpdate?: ShouldUpdate<T>): [T, SetStoreValue<T>];
|
|
35
35
|
|
|
36
|
-
export {
|
|
36
|
+
export { SetStoreValue, ShouldUpdate, ShouldUpdateCallback, useStore };
|
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
export * from "@t8/store"
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Returns the
|
|
8
|
-
*
|
|
7
|
+
* Returns the value of `store` passed as the parameter and a
|
|
8
|
+
* function to update the store state value.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```js
|
|
@@ -18,29 +18,29 @@ export * from "@t8/store"
|
|
|
18
18
|
*
|
|
19
19
|
* `shouldUpdate` can be set to `false` to prevent subscription
|
|
20
20
|
* to the store updates. Use case: if the component only requires
|
|
21
|
-
* the store
|
|
22
|
-
*
|
|
21
|
+
* the store value setter but not the store value, the component
|
|
22
|
+
* may not need to respond to the store updates at all:
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```js
|
|
26
26
|
* let [, setValue] = useStore(store, false);
|
|
27
27
|
* ```
|
|
28
28
|
*
|
|
29
|
-
* `shouldUpdate` can also be a function `(
|
|
30
|
-
* to make the component respond only to specific store
|
|
29
|
+
* `shouldUpdate` can also be a function `(nextValue, prevValue) => boolean`
|
|
30
|
+
* to make the component respond only to specific store value changes,
|
|
31
31
|
* when this function returns `true`.
|
|
32
32
|
*/
|
|
33
33
|
function useStore(store, shouldUpdate = true) {
|
|
34
34
|
if (!isStore(store)) throw new Error("'store' is not an instance of Store");
|
|
35
35
|
let [, setRevision] = useState(-1);
|
|
36
|
-
let
|
|
37
|
-
let
|
|
36
|
+
let value = store.getValue();
|
|
37
|
+
let setValue = useMemo(() => store.setValue.bind(store), [store]);
|
|
38
38
|
let initialStoreRevision = useRef(store.revision);
|
|
39
39
|
useEffect(() => {
|
|
40
40
|
if (isPersistentStore(store)) store.syncOnce();
|
|
41
41
|
if (!shouldUpdate) return;
|
|
42
|
-
let unsubscribe = store.onUpdate((
|
|
43
|
-
if (typeof shouldUpdate !== "function" || shouldUpdate(
|
|
42
|
+
let unsubscribe = store.onUpdate((nextValue, prevValue) => {
|
|
43
|
+
if (typeof shouldUpdate !== "function" || shouldUpdate(nextValue, prevValue)) setRevision(Math.random());
|
|
44
44
|
});
|
|
45
45
|
if (store.revision !== initialStoreRevision.current) setRevision(Math.random());
|
|
46
46
|
return () => {
|
|
@@ -48,7 +48,7 @@ function useStore(store, shouldUpdate = true) {
|
|
|
48
48
|
initialStoreRevision.current = store.revision;
|
|
49
49
|
};
|
|
50
50
|
}, [store, shouldUpdate]);
|
|
51
|
-
return [
|
|
51
|
+
return [value, setValue];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export { useStore };
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t8/react-store",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.8",
|
|
4
|
+
"description": "React app state management condensed to the essentials",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"demo": "npx @t8/serve
|
|
10
|
+
"demo": "npx @t8/serve tests/counter --spa -b src/index.tsx",
|
|
11
|
+
"demo-t3": "npx @t8/serve tests/tic-tac-toe --spa -b src/index.tsx",
|
|
11
12
|
"preversion": "npx npm-run-all shape test",
|
|
12
13
|
"shape": "npx codeshape",
|
|
13
|
-
"test": "npx playwright test --project=chromium"
|
|
14
|
-
"tic-tac-toe": "npx @t8/serve 3000 * tests/tic-tac-toe -b src/index.tsx"
|
|
14
|
+
"test": "npx playwright test --project=chromium"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@playwright/test": "^1.56.0",
|
|
35
|
-
"@t8/serve": "^0.
|
|
35
|
+
"@t8/serve": "^0.2.0",
|
|
36
36
|
"@types/node": "^24.5.2",
|
|
37
37
|
"@types/react": "^19.2.7",
|
|
38
38
|
"@types/react-dom": "^19.2.3",
|
|
39
39
|
"immer": "^11.0.1",
|
|
40
|
-
"react-dom": "^19.2.
|
|
40
|
+
"react-dom": "^19.2.3"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@t8/store": "^1.
|
|
43
|
+
"@t8/store": "^1.4.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/useStore.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isPersistentStore, isStore, type Store } from "@t8/store";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
|
|
4
|
-
export type
|
|
5
|
-
export type ShouldUpdateCallback<T> = (
|
|
4
|
+
export type SetStoreValue<T> = Store<T>["setValue"];
|
|
5
|
+
export type ShouldUpdateCallback<T> = (nextValue: T, prevValue: T) => boolean;
|
|
6
6
|
export type ShouldUpdate<T> = boolean | ShouldUpdateCallback<T>;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Returns the
|
|
10
|
-
*
|
|
9
|
+
* Returns the value of `store` passed as the parameter and a
|
|
10
|
+
* function to update the store state value.
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```js
|
|
@@ -20,29 +20,29 @@ export type ShouldUpdate<T> = boolean | ShouldUpdateCallback<T>;
|
|
|
20
20
|
*
|
|
21
21
|
* `shouldUpdate` can be set to `false` to prevent subscription
|
|
22
22
|
* to the store updates. Use case: if the component only requires
|
|
23
|
-
* the store
|
|
24
|
-
*
|
|
23
|
+
* the store value setter but not the store value, the component
|
|
24
|
+
* may not need to respond to the store updates at all:
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
27
27
|
* ```js
|
|
28
28
|
* let [, setValue] = useStore(store, false);
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
|
-
* `shouldUpdate` can also be a function `(
|
|
32
|
-
* to make the component respond only to specific store
|
|
31
|
+
* `shouldUpdate` can also be a function `(nextValue, prevValue) => boolean`
|
|
32
|
+
* to make the component respond only to specific store value changes,
|
|
33
33
|
* when this function returns `true`.
|
|
34
34
|
*/
|
|
35
35
|
export function useStore<T>(
|
|
36
36
|
store: Store<T>,
|
|
37
37
|
shouldUpdate: ShouldUpdate<T> = true,
|
|
38
|
-
): [T,
|
|
38
|
+
): [T, SetStoreValue<T>] {
|
|
39
39
|
if (!isStore<T>(store))
|
|
40
40
|
throw new Error("'store' is not an instance of Store");
|
|
41
41
|
|
|
42
42
|
let [, setRevision] = useState(-1);
|
|
43
43
|
|
|
44
|
-
let
|
|
45
|
-
let
|
|
44
|
+
let value = store.getValue();
|
|
45
|
+
let setValue = useMemo(() => store.setValue.bind(store), [store]);
|
|
46
46
|
let initialStoreRevision = useRef(store.revision);
|
|
47
47
|
|
|
48
48
|
useEffect(() => {
|
|
@@ -50,10 +50,10 @@ export function useStore<T>(
|
|
|
50
50
|
|
|
51
51
|
if (!shouldUpdate) return;
|
|
52
52
|
|
|
53
|
-
let unsubscribe = store.onUpdate((
|
|
53
|
+
let unsubscribe = store.onUpdate((nextValue, prevValue) => {
|
|
54
54
|
if (
|
|
55
55
|
typeof shouldUpdate !== "function" ||
|
|
56
|
-
shouldUpdate(
|
|
56
|
+
shouldUpdate(nextValue, prevValue)
|
|
57
57
|
)
|
|
58
58
|
setRevision(Math.random());
|
|
59
59
|
});
|
|
@@ -67,5 +67,5 @@ export function useStore<T>(
|
|
|
67
67
|
};
|
|
68
68
|
}, [store, shouldUpdate]);
|
|
69
69
|
|
|
70
|
-
return [
|
|
70
|
+
return [value, setValue];
|
|
71
71
|
}
|