@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 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
@@ -0,0 +1,4 @@
1
+ export * from './src/PendingState';
2
+ export * from './src/PendingStateContext';
3
+ export * from './src/PendingStateProvider';
4
+ export * from './src/usePendingState';
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,6 @@
1
+ export type PendingState = {
2
+ initialized?: boolean | undefined;
3
+ complete?: boolean | undefined;
4
+ time?: number | undefined;
5
+ error?: unknown;
6
+ };
@@ -0,0 +1,7 @@
1
+ import type {Store} from '@t8/react-store';
2
+ import {createContext} from 'react';
3
+ import type {PendingState} from './PendingState';
4
+
5
+ export const PendingStateContext = createContext(
6
+ new Map<string, Store<PendingState>>(),
7
+ );
@@ -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
+ }