@t8/react-pending 0.1.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 +0 -0
- package/dist/index.js +170 -0
- package/index.ts +4 -0
- package/package.json +33 -0
- package/src/PendingState.ts +6 -0
- package/src/PendingStateContext.ts +7 -0
- package/src/PendingStateProvider.tsx +43 -0
- package/src/usePendingState.ts +129 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
File without changes
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/PendingStateContext.ts
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
var PendingStateContext = createContext(
|
|
4
|
+
/* @__PURE__ */ new Map()
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
// node_modules/@t8/store/src/isStore.ts
|
|
8
|
+
function isStore(x) {
|
|
9
|
+
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";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// node_modules/@t8/store/src/Store.ts
|
|
13
|
+
var Store = class {
|
|
14
|
+
state;
|
|
15
|
+
callbacks;
|
|
16
|
+
constructor(data) {
|
|
17
|
+
this.state = data;
|
|
18
|
+
this.callbacks = [];
|
|
19
|
+
}
|
|
20
|
+
onUpdate(callback) {
|
|
21
|
+
this.callbacks.push(callback);
|
|
22
|
+
return () => {
|
|
23
|
+
for (let i = this.callbacks.length - 1; i >= 0; i--) {
|
|
24
|
+
if (this.callbacks[i] === callback) this.callbacks.splice(i, 1);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
getState() {
|
|
29
|
+
return this.state;
|
|
30
|
+
}
|
|
31
|
+
setState(update) {
|
|
32
|
+
let prevState = this.state;
|
|
33
|
+
let nextState = update instanceof Function ? update(this.state) : update;
|
|
34
|
+
this.state = nextState;
|
|
35
|
+
for (let callback of this.callbacks) callback(nextState, prevState);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// node_modules/@t8/react-store/src/useStore.ts
|
|
40
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
41
|
+
function useStore(store, shouldUpdate = true) {
|
|
42
|
+
if (!isStore(store)) throw new Error("'store' is not an instance of Store");
|
|
43
|
+
let [, setRevision] = useState(-1);
|
|
44
|
+
let initedRef = useRef(false);
|
|
45
|
+
let state = store.getState();
|
|
46
|
+
let setState = useMemo(() => store.setState.bind(store), [store]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!shouldUpdate) return;
|
|
49
|
+
let unsubscribe = store.onUpdate((nextState, prevState) => {
|
|
50
|
+
if (typeof shouldUpdate !== "function" || shouldUpdate(nextState, prevState))
|
|
51
|
+
setRevision(Math.random());
|
|
52
|
+
});
|
|
53
|
+
if (!initedRef.current) {
|
|
54
|
+
initedRef.current = true;
|
|
55
|
+
setRevision(Math.random());
|
|
56
|
+
}
|
|
57
|
+
return () => {
|
|
58
|
+
unsubscribe();
|
|
59
|
+
initedRef.current = false;
|
|
60
|
+
};
|
|
61
|
+
}, [store, shouldUpdate]);
|
|
62
|
+
return [state, setState];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/PendingStateProvider.tsx
|
|
66
|
+
import { useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
67
|
+
import { jsx } from "react/jsx-runtime";
|
|
68
|
+
var PendingStateProvider = ({
|
|
69
|
+
value,
|
|
70
|
+
children
|
|
71
|
+
}) => {
|
|
72
|
+
let defaultValueRef = useRef2(null);
|
|
73
|
+
let resolvedValue = useMemo2(() => {
|
|
74
|
+
if (value instanceof Map) return value;
|
|
75
|
+
if (typeof value === "object" && value !== null)
|
|
76
|
+
return new Map(
|
|
77
|
+
Object.entries(value).map(([key, state]) => [
|
|
78
|
+
key,
|
|
79
|
+
new Store(state)
|
|
80
|
+
])
|
|
81
|
+
);
|
|
82
|
+
if (defaultValueRef.current === null)
|
|
83
|
+
defaultValueRef.current = /* @__PURE__ */ new Map();
|
|
84
|
+
return defaultValueRef.current;
|
|
85
|
+
}, [value]);
|
|
86
|
+
return /* @__PURE__ */ jsx(PendingStateContext.Provider, { value: resolvedValue, children });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/usePendingState.ts
|
|
90
|
+
import { useCallback, useContext, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
|
|
91
|
+
function createState(initialized = false, complete = false, error) {
|
|
92
|
+
return {
|
|
93
|
+
initialized,
|
|
94
|
+
complete,
|
|
95
|
+
error,
|
|
96
|
+
time: Date.now()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function usePendingState(store) {
|
|
100
|
+
let storeMap = useContext(PendingStateContext);
|
|
101
|
+
let storeRef = useRef3(null);
|
|
102
|
+
let [storeItemInited, setStoreItemInited] = useState2(false);
|
|
103
|
+
let resolvedStore = useMemo3(() => {
|
|
104
|
+
if (isStore(store)) return store;
|
|
105
|
+
if (typeof store === "string") {
|
|
106
|
+
let storeItem = storeMap.get(store);
|
|
107
|
+
if (!storeItem) {
|
|
108
|
+
storeItem = new Store(createState());
|
|
109
|
+
storeMap.set(store, storeItem);
|
|
110
|
+
if (!storeItemInited) setStoreItemInited(true);
|
|
111
|
+
}
|
|
112
|
+
return storeItem;
|
|
113
|
+
}
|
|
114
|
+
if (!storeRef.current) storeRef.current = new Store(createState());
|
|
115
|
+
return storeRef.current;
|
|
116
|
+
}, [store, storeMap, storeItemInited]);
|
|
117
|
+
let [state, setState] = useStore(resolvedStore);
|
|
118
|
+
let withState = useCallback(
|
|
119
|
+
(value, options) => {
|
|
120
|
+
if (value instanceof Promise) {
|
|
121
|
+
let delayedPending = null;
|
|
122
|
+
if (!options?.silent) {
|
|
123
|
+
let delay = options?.delay;
|
|
124
|
+
if (delay === void 0)
|
|
125
|
+
setState((prevState) => ({
|
|
126
|
+
...prevState,
|
|
127
|
+
...createState(true, false)
|
|
128
|
+
}));
|
|
129
|
+
else
|
|
130
|
+
delayedPending = setTimeout(() => {
|
|
131
|
+
setState((prevState) => ({
|
|
132
|
+
...prevState,
|
|
133
|
+
...createState(true, false)
|
|
134
|
+
}));
|
|
135
|
+
delayedPending = null;
|
|
136
|
+
}, delay);
|
|
137
|
+
}
|
|
138
|
+
return value.then((resolvedValue) => {
|
|
139
|
+
if (delayedPending !== null)
|
|
140
|
+
clearTimeout(delayedPending);
|
|
141
|
+
setState((prevState) => ({
|
|
142
|
+
...prevState,
|
|
143
|
+
...createState(true, true)
|
|
144
|
+
}));
|
|
145
|
+
return resolvedValue;
|
|
146
|
+
}).catch((error) => {
|
|
147
|
+
if (delayedPending !== null)
|
|
148
|
+
clearTimeout(delayedPending);
|
|
149
|
+
setState((prevState) => ({
|
|
150
|
+
...prevState,
|
|
151
|
+
...createState(true, true, error)
|
|
152
|
+
}));
|
|
153
|
+
if (options?.throws) throw error;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
setState((prevState) => ({
|
|
157
|
+
...prevState,
|
|
158
|
+
...createState(true, true)
|
|
159
|
+
}));
|
|
160
|
+
return value;
|
|
161
|
+
},
|
|
162
|
+
[setState]
|
|
163
|
+
);
|
|
164
|
+
return [state, withState, setState];
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
PendingStateContext,
|
|
168
|
+
PendingStateProvider,
|
|
169
|
+
usePendingState
|
|
170
|
+
};
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@t8/react-pending",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Concise async action state tracking 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
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"preversion": "npx npm-run-all shape build",
|
|
13
|
+
"shape": "npx codeshape"
|
|
14
|
+
},
|
|
15
|
+
"author": "axtk",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/t8dev/react-pending.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"async actions",
|
|
23
|
+
"pending state",
|
|
24
|
+
"react"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@t8/react-store": "^1.0.0",
|
|
28
|
+
"react": ">=16.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^19.1.10"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {Store} from '@t8/react-store';
|
|
2
|
+
import {type ReactNode, useMemo, useRef} from 'react';
|
|
3
|
+
import type {PendingState} from './PendingState';
|
|
4
|
+
import {PendingStateContext} from './PendingStateContext';
|
|
5
|
+
|
|
6
|
+
export type PendingStateProviderProps = {
|
|
7
|
+
value?:
|
|
8
|
+
| Record<string, PendingState>
|
|
9
|
+
| Map<string, Store<PendingState>>
|
|
10
|
+
| null
|
|
11
|
+
| undefined;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const PendingStateProvider = ({
|
|
16
|
+
value,
|
|
17
|
+
children,
|
|
18
|
+
}: PendingStateProviderProps) => {
|
|
19
|
+
let defaultValueRef = useRef<Map<string, Store<PendingState>> | null>(null);
|
|
20
|
+
|
|
21
|
+
let resolvedValue = useMemo(() => {
|
|
22
|
+
if (value instanceof Map) return value;
|
|
23
|
+
|
|
24
|
+
if (typeof value === 'object' && value !== null)
|
|
25
|
+
return new Map(
|
|
26
|
+
Object.entries(value).map(([key, state]) => [
|
|
27
|
+
key,
|
|
28
|
+
new Store(state),
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (defaultValueRef.current === null)
|
|
33
|
+
defaultValueRef.current = new Map<string, Store<PendingState>>();
|
|
34
|
+
|
|
35
|
+
return defaultValueRef.current;
|
|
36
|
+
}, [value]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<PendingStateContext.Provider value={resolvedValue}>
|
|
40
|
+
{children}
|
|
41
|
+
</PendingStateContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {isStore, type SetStoreState, Store, useStore} from '@t8/react-store';
|
|
2
|
+
import {useCallback, useContext, useMemo, useRef, useState} from 'react';
|
|
3
|
+
import type {PendingState} from './PendingState';
|
|
4
|
+
import {PendingStateContext} from './PendingStateContext';
|
|
5
|
+
|
|
6
|
+
function createState(
|
|
7
|
+
initialized = false,
|
|
8
|
+
complete = false,
|
|
9
|
+
error?: unknown,
|
|
10
|
+
): PendingState {
|
|
11
|
+
return {
|
|
12
|
+
initialized,
|
|
13
|
+
complete,
|
|
14
|
+
error,
|
|
15
|
+
time: Date.now(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WithStateOptions = {
|
|
20
|
+
silent?: boolean;
|
|
21
|
+
throws?: boolean;
|
|
22
|
+
delay?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns an array containing `[state, withState, setState]`:
|
|
27
|
+
* - `state` reflects the state of a value passed to `withState()`;
|
|
28
|
+
* - `withState(value, [options])` enables the tracking of the state
|
|
29
|
+
* of `value`; setting the options to `{silent: true}` prevents
|
|
30
|
+
* `withState()` from updating the state while `value` is pending
|
|
31
|
+
* (e.g. for background or optimistic updates);
|
|
32
|
+
* - `setState()` to directly update the state.
|
|
33
|
+
*/
|
|
34
|
+
export function usePendingState(
|
|
35
|
+
/**
|
|
36
|
+
* A unique store key or a store. Providing a store key or a
|
|
37
|
+
* shared store allows to share the state across multiple
|
|
38
|
+
* components.
|
|
39
|
+
*/
|
|
40
|
+
store?: string | Store<PendingState> | null,
|
|
41
|
+
): [PendingState, <T>(value: T) => T, SetStoreState<PendingState>] {
|
|
42
|
+
let storeMap = useContext(PendingStateContext);
|
|
43
|
+
let storeRef = useRef<Store<PendingState> | null>(null);
|
|
44
|
+
let [storeItemInited, setStoreItemInited] = useState(false);
|
|
45
|
+
|
|
46
|
+
let resolvedStore = useMemo(() => {
|
|
47
|
+
if (isStore<PendingState>(store)) return store;
|
|
48
|
+
|
|
49
|
+
if (typeof store === 'string') {
|
|
50
|
+
let storeItem = storeMap.get(store);
|
|
51
|
+
|
|
52
|
+
if (!storeItem) {
|
|
53
|
+
storeItem = new Store(createState());
|
|
54
|
+
storeMap.set(store, storeItem);
|
|
55
|
+
|
|
56
|
+
if (!storeItemInited) setStoreItemInited(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return storeItem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!storeRef.current) storeRef.current = new Store(createState());
|
|
63
|
+
|
|
64
|
+
return storeRef.current;
|
|
65
|
+
}, [store, storeMap, storeItemInited]);
|
|
66
|
+
|
|
67
|
+
let [state, setState] = useStore(resolvedStore);
|
|
68
|
+
|
|
69
|
+
let withState = useCallback(
|
|
70
|
+
<T>(value: T, options?: WithStateOptions): T => {
|
|
71
|
+
if (value instanceof Promise) {
|
|
72
|
+
let delayedPending: ReturnType<typeof setTimeout> | null = null;
|
|
73
|
+
|
|
74
|
+
if (!options?.silent) {
|
|
75
|
+
let delay = options?.delay;
|
|
76
|
+
|
|
77
|
+
if (delay === undefined)
|
|
78
|
+
setState(prevState => ({
|
|
79
|
+
...prevState,
|
|
80
|
+
...createState(true, false),
|
|
81
|
+
}));
|
|
82
|
+
else
|
|
83
|
+
delayedPending = setTimeout(() => {
|
|
84
|
+
setState(prevState => ({
|
|
85
|
+
...prevState,
|
|
86
|
+
...createState(true, false),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
delayedPending = null;
|
|
90
|
+
}, delay);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value
|
|
94
|
+
.then(resolvedValue => {
|
|
95
|
+
if (delayedPending !== null)
|
|
96
|
+
clearTimeout(delayedPending);
|
|
97
|
+
|
|
98
|
+
setState(prevState => ({
|
|
99
|
+
...prevState,
|
|
100
|
+
...createState(true, true),
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
return resolvedValue;
|
|
104
|
+
})
|
|
105
|
+
.catch(error => {
|
|
106
|
+
if (delayedPending !== null)
|
|
107
|
+
clearTimeout(delayedPending);
|
|
108
|
+
|
|
109
|
+
setState(prevState => ({
|
|
110
|
+
...prevState,
|
|
111
|
+
...createState(true, true, error),
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (options?.throws) throw error;
|
|
115
|
+
}) as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setState(prevState => ({
|
|
119
|
+
...prevState,
|
|
120
|
+
...createState(true, true),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
return value;
|
|
124
|
+
},
|
|
125
|
+
[setState],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return [state, withState, setState];
|
|
129
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["index.ts"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true
|
|
12
|
+
}
|
|
13
|
+
}
|