@t8/react-store 1.0.12 → 1.0.13
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 +34 -34
- package/index.ts +2 -2
- package/package.json +47 -39
- package/src/useStore.ts +51 -51
- package/tsconfig.json +13 -12
package/README.md
CHANGED
|
@@ -16,34 +16,34 @@ Installation: `npm i @t8/react-store`
|
|
|
16
16
|
Moving the local state to the full-fledged shared state:
|
|
17
17
|
|
|
18
18
|
```diff
|
|
19
|
-
import {createContext, useContext} from
|
|
20
|
-
+ import {Store, useStore} from
|
|
19
|
+
import { createContext, useContext } from "react";
|
|
20
|
+
+ import { Store, useStore } from "@t8/react-store";
|
|
21
21
|
+
|
|
22
22
|
+ let AppContext = createContext(new Store(0));
|
|
23
23
|
|
|
24
24
|
let Counter = () => {
|
|
25
|
-
-
|
|
26
|
-
+
|
|
25
|
+
- let [counter, setCounter] = useState(0);
|
|
26
|
+
+ let [counter, setCounter] = useStore(useContext(AppContext));
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
let handleClick = () => {
|
|
29
|
+
setCounter(value => value + 1);
|
|
30
|
+
};
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
return <button onClick={handleClick}>{counter}</button>;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
let ResetButton = () => {
|
|
36
|
-
-
|
|
37
|
-
+
|
|
36
|
+
- let [, setCounter] = useState(0);
|
|
37
|
+
+ let [, setCounter] = useStore(useContext(AppContext), false);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
let handleClick = () => {
|
|
40
|
+
setCounter(0);
|
|
41
|
+
};
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
return <button onClick={handleClick}>×</button>;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
let App = () => <><Counter/>{
|
|
46
|
+
let App = () => <><Counter/>{" "}<ResetButton/></>;
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
[Live demo](https://codesandbox.io/p/sandbox/rtng37?file=%2Fsrc%2FPlusButton.jsx)
|
|
@@ -56,27 +56,27 @@ Moving the local state to the full-fledged shared state:
|
|
|
56
56
|
|
|
57
57
|
```js
|
|
58
58
|
let AppContext = createContext({
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
users: new Store(/* ... */),
|
|
60
|
+
items: new Store(/* ... */),
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
🔹 Apart from a boolean, `useStore(store, shouldUpdate)` can take a function of `(nextState, prevState) => boolean` as the second parameter to filter store updates to respond to:
|
|
65
65
|
|
|
66
66
|
```jsx
|
|
67
|
-
let ItemCard = ({id}) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
let ItemCard = ({ id }) => {
|
|
68
|
+
let hasRelevantUpdates = useCallback((nextItems, prevItems) => {
|
|
69
|
+
return nextItems[id].revision !== prevItems[id].revision;
|
|
70
|
+
}, [id]);
|
|
71
|
+
|
|
72
|
+
let [items, setItems] = useStore(
|
|
73
|
+
useContext(AppContext).items,
|
|
74
|
+
hasRelevantUpdates,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
// content
|
|
79
|
+
);
|
|
80
80
|
};
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -84,10 +84,10 @@ let ItemCard = ({id}) => {
|
|
|
84
84
|
|
|
85
85
|
```diff
|
|
86
86
|
let App = () => (
|
|
87
|
-
-
|
|
88
|
-
+
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
- <AppContext.Provider value={42}>
|
|
88
|
+
+ <AppContext.Provider value={new Store(42)}>
|
|
89
|
+
<PlusButton/>{" "}<Display/>
|
|
90
|
+
</AppContext.Provider>
|
|
91
91
|
);
|
|
92
92
|
```
|
|
93
93
|
|
package/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
1
|
+
export * from "@t8/store";
|
|
2
|
+
export * from "./src/useStore";
|
package/package.json
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@t8/react-store",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Straightforward and minimalist shared state management for React apps",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "npx npm-run-all clean compile",
|
|
9
|
-
"clean": "node -e \"require('node:fs').rmSync('dist', {force: true, recursive: true});\"",
|
|
10
|
-
"compile": "npx esbuild index.ts --bundle --outdir=dist --platform=neutral --external:react",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"@
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@t8/react-store",
|
|
3
|
+
"version": "1.0.13",
|
|
4
|
+
"description": "Straightforward and minimalist shared state management for React apps",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "npx npm-run-all clean compile",
|
|
9
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', {force: true, recursive: true});\"",
|
|
10
|
+
"compile": "npx esbuild index.ts --bundle --outdir=dist --platform=neutral --external:react",
|
|
11
|
+
"demo": "npx @t8/serve 3000 * tests/counter -b src/index.tsx",
|
|
12
|
+
"gh-pages": "npx ghstage --theme=t8 --ymid=103784239 --nav=https://raw.githubusercontent.com/t8js/t8js.github.io/refs/heads/main/assets/nav.html",
|
|
13
|
+
"prepublishOnly": "npx npm-run-all build gh-pages",
|
|
14
|
+
"preversion": "npx npm-run-all typecheck shape build test",
|
|
15
|
+
"shape": "npx codeshape",
|
|
16
|
+
"test": "npx playwright test --project=chromium",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/t8js/react-store.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://t8.js.org/react-store",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"shared state",
|
|
27
|
+
"store"
|
|
28
|
+
],
|
|
29
|
+
"author": "axtk",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=16.8"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@playwright/test": "^1.55.1",
|
|
36
|
+
"@t8/serve": "^0.1.19",
|
|
37
|
+
"@types/node": "^24.5.2",
|
|
38
|
+
"@types/react": "^19.1.10",
|
|
39
|
+
"@types/react-dom": "^19.1.9",
|
|
40
|
+
"immer": "^10.1.3",
|
|
41
|
+
"react-dom": "^19.1.1",
|
|
42
|
+
"typescript": "^5.9.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@t8/store": "^1.1.3"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/useStore.ts
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import {isStore, type Store} from
|
|
2
|
-
import {useEffect, useMemo, useRef, useState} from
|
|
1
|
+
import { isStore, type Store } from "@t8/store";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
|
|
4
|
-
export type SetStoreState<T> = Store<T>[
|
|
4
|
+
export type SetStoreState<T> = Store<T>["setState"];
|
|
5
5
|
export type ShouldUpdateCallback<T> = (nextState: T, prevState: T) => boolean;
|
|
6
6
|
export type ShouldUpdate<T> = boolean | ShouldUpdateCallback<T>;
|
|
7
7
|
|
|
8
8
|
export function useStore<T>(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
store: Store<T>,
|
|
10
|
+
/**
|
|
11
|
+
* Controls whether the component should be updated in response
|
|
12
|
+
* to the store updates.
|
|
13
|
+
*
|
|
14
|
+
* @defaultValue `true`
|
|
15
|
+
*
|
|
16
|
+
* Can be set to `false` when the component only requires the
|
|
17
|
+
* store state setter but not the state value itself, and so the
|
|
18
|
+
* component doesn't need to respond to updates in the store state.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* let [, setValue] = useStore(store, false);
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Can be set to a function `(nextState, prevState) => boolean` to
|
|
25
|
+
* make the component respond only to specific store state changes.
|
|
26
|
+
*/
|
|
27
|
+
shouldUpdate: ShouldUpdate<T> = true,
|
|
28
28
|
): [T, SetStoreState<T>] {
|
|
29
|
-
|
|
29
|
+
if (!isStore(store)) throw new Error("'store' is not an instance of Store");
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let [, setRevision] = useState(-1);
|
|
32
|
+
let initedRef = useRef(false);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
let state = store.getState();
|
|
35
|
+
let setState = useMemo(() => store.setState.bind(store), [store]);
|
|
36
|
+
let initialStoreRevision = useRef(store.revision);
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!shouldUpdate) return;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
let unsubscribe = store.onUpdate((nextState, prevState) => {
|
|
42
|
+
if (
|
|
43
|
+
typeof shouldUpdate !== "function" ||
|
|
44
|
+
shouldUpdate(nextState, prevState)
|
|
45
|
+
)
|
|
46
|
+
setRevision(Math.random());
|
|
47
|
+
});
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Trigger a rerender if the store state changed after the component
|
|
50
|
+
// last read it and before the component subscribed to its changes.
|
|
51
|
+
if (!initedRef.current) {
|
|
52
|
+
initedRef.current = true;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (store.revision !== initialStoreRevision.current)
|
|
55
|
+
setRevision(Math.random());
|
|
56
|
+
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
return () => {
|
|
59
|
+
unsubscribe();
|
|
60
|
+
initedRef.current = false;
|
|
61
|
+
initialStoreRevision.current = store.revision;
|
|
62
|
+
};
|
|
63
|
+
}, [store, shouldUpdate]);
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
return [state, setState];
|
|
66
66
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"include": ["index.ts", "playwright.config.ts", "src", "tests"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"lib": ["ESNext", "DOM"],
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true
|
|
12
|
+
}
|
|
13
|
+
}
|