@t8/react-pending 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 CHANGED
@@ -13,43 +13,43 @@ Installation: `npm i @t8/react-pending`
13
13
  Objective: Track the pending state of the async `fetchItems()` action to tell the user whether the UI is busy or encountered an error (preferably without rewriting the action and the app's state management).
14
14
 
15
15
  ```diff
16
- + import {usePendingState} from '@t8/react-pending';
16
+ + import { usePendingState } from "@t8/react-pending";
17
17
 
18
18
  const ItemList = () => {
19
- const [items, setItems] = useState([]);
20
- // the custom string key parameter tags the action's state so
21
- // that another component can access this state by the same tag
22
- + const [state, withState] = usePendingState('fetch-items');
19
+ const [items, setItems] = useState([]);
20
+ // the custom string key parameter tags the action's state so
21
+ // that another component can access this state by the same tag
22
+ + const [state, withState] = usePendingState("fetch-items");
23
23
 
24
- useEffect(() => {
25
- // wrapping fetchItems() to track the async action's state
26
- - fetchItems().then(setItems);
27
- + withState(fetchItems()).then(setItems);
28
- }, [fetchItems, withState]);
24
+ useEffect(() => {
25
+ // wrapping fetchItems() to track the async action's state
26
+ - fetchItems().then(setItems);
27
+ + withState(fetchItems()).then(setItems);
28
+ }, [fetchItems, withState]);
29
29
 
30
- + if (!state.complete)
31
- + return <p>Loading...</p>;
30
+ + if (!state.complete)
31
+ + return <p>Loading...</p>;
32
32
 
33
- + if (state.error)
34
- + return <p>An error occurred</p>;
33
+ + if (state.error)
34
+ + return <p>An error occurred</p>;
35
35
 
36
- return <ul>{items.map(/* ... */)}</ul>;
36
+ return <ul>{items.map(/* ... */)}</ul>;
37
37
  };
38
38
 
39
39
  const Status = () => {
40
- // reading the 'fetch-items' state updated in ItemList
41
- + const [state] = usePendingState('fetch-items');
40
+ // reading the "fetch-items" state updated in ItemList
41
+ + const [state] = usePendingState("fetch-items");
42
42
 
43
- if (!state.initialized)
44
- return 'Initial';
43
+ if (!state.initialized)
44
+ return "Initial";
45
45
 
46
- if (!state.complete)
47
- return 'Busy';
46
+ if (!state.complete)
47
+ return "Busy";
48
48
 
49
- if (state.error)
50
- return 'Error';
49
+ if (state.error)
50
+ return "Error";
51
51
 
52
- return 'OK';
52
+ return "OK";
53
53
  };
54
54
  ```
55
55
 
@@ -58,7 +58,7 @@ Objective: Track the pending state of the async `fetchItems()` action to tell th
58
58
  🔹 If the action's state is only used within a single component, it can be used locally by omitting the custom string key parameter of the `usePendingState()` hook.
59
59
 
60
60
  ```diff
61
- - const [state, withState] = usePendingState('fetch-items');
61
+ - const [state, withState] = usePendingState("fetch-items");
62
62
  + const [state, withState] = usePendingState();
63
63
  ```
64
64
 
@@ -68,27 +68,27 @@ Objective: Track the pending state of the async `fetchItems()` action to tell th
68
68
 
69
69
  ```diff
70
70
  - withState(fetchItems())
71
- + withState(fetchItems(), {silent: true})
71
+ + withState(fetchItems(), { silent: true })
72
72
  ```
73
73
 
74
74
  🔹 Revealing the action's pending state after a delay (e.g. to avoid flashing a process indicator when the action is likely to complete by the end of the delay):
75
75
 
76
76
  ```diff
77
77
  - withState(fetchItems())
78
- + withState(fetchItems(), {delay: 500})
78
+ + withState(fetchItems(), { delay: 500 })
79
79
  ```
80
80
 
81
81
  🔹 Allowing the action's Promise value to reject explicitly (e.g. in order to provide the action with a custom rejection handler) along with exposing `state.error` that goes by default:
82
82
 
83
83
  ```diff
84
84
  - withState(fetchItems())
85
- + withState(fetchItems(), {throws: true}).catch(handleError)
85
+ + withState(fetchItems(), { throws: true }).catch(handleError)
86
86
  ```
87
87
 
88
88
  🔹 Providing an isolated instance of initial shared action state, e.g. for tests or SSR (it can be unnecessary for client-side rendering where the default context value is sufficient, but it can also be used to separate action states of larger self-contained portions of a web app):
89
89
 
90
90
  ```diff
91
- + import {PendingStateProvider} from '@t8/react-pending';
91
+ + import { PendingStateProvider } from "@t8/react-pending";
92
92
 
93
93
  - <App/>
94
94
  + <PendingStateProvider>
@@ -100,7 +100,7 @@ Objective: Track the pending state of the async `fetchItems()` action to tell th
100
100
 
101
101
  ```diff
102
102
  + const initialState = {
103
- + 'fetch-items': { initialized: true, complete: true },
103
+ + "fetch-items": { initialized: true, complete: true },
104
104
  + };
105
105
 
106
106
  - <PendingStateProvider>
@@ -109,4 +109,4 @@ Objective: Track the pending state of the async `fetchItems()` action to tell th
109
109
  </PendingStateProvider>
110
110
  ```
111
111
 
112
- With an explicit value or without, the `<PendingStateProvider>`'s nested components will only respond to updates in the particular action states they subscribed to by means of `usePendingState('action-key')`.
112
+ With an explicit value or without, the `<PendingStateProvider>`'s nested components will only respond to updates in the particular action states they subscribed to by means of `usePendingState("action-key")`.
package/dist/index.js CHANGED
@@ -78,10 +78,7 @@ var PendingStateProvider = ({
78
78
  if (value instanceof Map) return value;
79
79
  if (typeof value === "object" && value !== null)
80
80
  return new Map(
81
- Object.entries(value).map(([key, state]) => [
82
- key,
83
- new Store(state)
84
- ])
81
+ Object.entries(value).map(([key, state]) => [key, new Store(state)])
85
82
  );
86
83
  if (defaultValueRef.current === null)
87
84
  defaultValueRef.current = /* @__PURE__ */ new Map();
@@ -140,16 +137,14 @@ function usePendingState(store) {
140
137
  }, delay);
141
138
  }
142
139
  return value.then((resolvedValue) => {
143
- if (delayedPending !== null)
144
- clearTimeout(delayedPending);
140
+ if (delayedPending !== null) clearTimeout(delayedPending);
145
141
  setState((prevState) => ({
146
142
  ...prevState,
147
143
  ...createState(true, true)
148
144
  }));
149
145
  return resolvedValue;
150
146
  }).catch((error) => {
151
- if (delayedPending !== null)
152
- clearTimeout(delayedPending);
147
+ if (delayedPending !== null) clearTimeout(delayedPending);
153
148
  setState((prevState) => ({
154
149
  ...prevState,
155
150
  ...createState(true, true, error)
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './src/PendingState';
2
- export * from './src/PendingStateContext';
3
- export * from './src/PendingStateProvider';
4
- export * from './src/usePendingState';
1
+ export * from "./src/PendingState";
2
+ export * from "./src/PendingStateContext";
3
+ export * from "./src/PendingStateProvider";
4
+ export * from "./src/usePendingState";
package/package.json CHANGED
@@ -1,39 +1,46 @@
1
- {
2
- "name": "@t8/react-pending",
3
- "version": "1.0.12",
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
- "gh-pages": "npx ghstage --theme=t8 --ymid=103784239 --nav=https://raw.githubusercontent.com/t8js/t8js.github.io/refs/heads/main/assets/nav.html",
12
- "prepublishOnly": "npx npm-run-all build gh-pages",
13
- "preversion": "npx npm-run-all typecheck shape build",
14
- "shape": "npx codeshape",
15
- "typecheck": "tsc --noEmit"
16
- },
17
- "author": "axtk",
18
- "license": "ISC",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/t8js/react-pending.git"
22
- },
23
- "homepage": "https://t8.js.org/react-pending",
24
- "keywords": [
25
- "async actions",
26
- "pending state",
27
- "react"
28
- ],
29
- "peerDependencies": {
30
- "react": ">=16.8"
31
- },
32
- "devDependencies": {
33
- "@types/react": "^19.1.10",
34
- "typescript": "^5.9.2"
35
- },
36
- "dependencies": {
37
- "@t8/react-store": "^1.0.12"
38
- }
39
- }
1
+ {
2
+ "name": "@t8/react-pending",
3
+ "version": "1.0.13",
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
+ "demo": "npx @t8/serve 3000 * tests/async_status -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
+ "author": "axtk",
20
+ "license": "ISC",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/t8js/react-pending.git"
24
+ },
25
+ "homepage": "https://t8.js.org/react-pending",
26
+ "keywords": [
27
+ "async actions",
28
+ "pending state",
29
+ "react"
30
+ ],
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
+ "react-dom": "^19.1.1",
41
+ "typescript": "^5.9.2"
42
+ },
43
+ "dependencies": {
44
+ "@t8/react-store": "^1.0.15"
45
+ }
46
+ }
@@ -1,6 +1,6 @@
1
1
  export type PendingState = {
2
- initialized?: boolean | undefined;
3
- complete?: boolean | undefined;
4
- time?: number | undefined;
5
- error?: unknown;
2
+ initialized?: boolean | undefined;
3
+ complete?: boolean | undefined;
4
+ time?: number | undefined;
5
+ error?: unknown;
6
6
  };
@@ -1,7 +1,7 @@
1
- import type {Store} from '@t8/react-store';
2
- import {createContext} from 'react';
3
- import type {PendingState} from './PendingState';
1
+ import type { Store } from "@t8/react-store";
2
+ import { createContext } from "react";
3
+ import type { PendingState } from "./PendingState";
4
4
 
5
5
  export const PendingStateContext = createContext(
6
- new Map<string, Store<PendingState>>(),
6
+ new Map<string, Store<PendingState>>(),
7
7
  );
@@ -1,43 +1,40 @@
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';
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
5
 
6
6
  export type PendingStateProviderProps = {
7
- value?:
8
- | Record<string, PendingState>
9
- | Map<string, Store<PendingState>>
10
- | null
11
- | undefined;
12
- children?: ReactNode;
7
+ value?:
8
+ | Record<string, PendingState>
9
+ | Map<string, Store<PendingState>>
10
+ | null
11
+ | undefined;
12
+ children?: ReactNode;
13
13
  };
14
14
 
15
15
  export const PendingStateProvider = ({
16
- value,
17
- children,
16
+ value,
17
+ children,
18
18
  }: PendingStateProviderProps) => {
19
- let defaultValueRef = useRef<Map<string, Store<PendingState>> | null>(null);
19
+ let defaultValueRef = useRef<Map<string, Store<PendingState>> | null>(null);
20
20
 
21
- let resolvedValue = useMemo(() => {
22
- if (value instanceof Map) return value;
21
+ let resolvedValue = useMemo(() => {
22
+ if (value instanceof Map) return value;
23
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
- );
24
+ if (typeof value === "object" && value !== null)
25
+ return new Map(
26
+ Object.entries(value).map(([key, state]) => [key, new Store(state)]),
27
+ );
31
28
 
32
- if (defaultValueRef.current === null)
33
- defaultValueRef.current = new Map<string, Store<PendingState>>();
29
+ if (defaultValueRef.current === null)
30
+ defaultValueRef.current = new Map<string, Store<PendingState>>();
34
31
 
35
- return defaultValueRef.current;
36
- }, [value]);
32
+ return defaultValueRef.current;
33
+ }, [value]);
37
34
 
38
- return (
39
- <PendingStateContext.Provider value={resolvedValue}>
40
- {children}
41
- </PendingStateContext.Provider>
42
- );
35
+ return (
36
+ <PendingStateContext.Provider value={resolvedValue}>
37
+ {children}
38
+ </PendingStateContext.Provider>
39
+ );
43
40
  };
@@ -1,25 +1,25 @@
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';
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
5
 
6
6
  function createState(
7
- initialized = false,
8
- complete = false,
9
- error?: unknown,
7
+ initialized = false,
8
+ complete = false,
9
+ error?: unknown,
10
10
  ): PendingState {
11
- return {
12
- initialized,
13
- complete,
14
- error,
15
- time: Date.now(),
16
- };
11
+ return {
12
+ initialized,
13
+ complete,
14
+ error,
15
+ time: Date.now(),
16
+ };
17
17
  }
18
18
 
19
19
  export type WithStateOptions = {
20
- silent?: boolean;
21
- throws?: boolean;
22
- delay?: number;
20
+ silent?: boolean;
21
+ throws?: boolean;
22
+ delay?: number;
23
23
  };
24
24
 
25
25
  /**
@@ -32,98 +32,96 @@ export type WithStateOptions = {
32
32
  * - `setState()` to directly update the state.
33
33
  */
34
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,
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
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);
42
+ let storeMap = useContext(PendingStateContext);
43
+ let storeRef = useRef<Store<PendingState> | null>(null);
44
+ let [storeItemInited, setStoreItemInited] = useState(false);
45
45
 
46
- let resolvedStore = useMemo(() => {
47
- if (isStore<PendingState>(store)) return store;
46
+ let resolvedStore = useMemo(() => {
47
+ if (isStore<PendingState>(store)) return store;
48
48
 
49
- if (typeof store === 'string') {
50
- let storeItem = storeMap.get(store);
49
+ if (typeof store === "string") {
50
+ let storeItem = storeMap.get(store);
51
51
 
52
- if (!storeItem) {
53
- storeItem = new Store(createState());
54
- storeMap.set(store, storeItem);
52
+ if (!storeItem) {
53
+ storeItem = new Store(createState());
54
+ storeMap.set(store, storeItem);
55
55
 
56
- if (!storeItemInited) setStoreItemInited(true);
57
- }
56
+ if (!storeItemInited) setStoreItemInited(true);
57
+ }
58
58
 
59
- return storeItem;
60
- }
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);
61
68
 
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 => ({
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) => ({
119
85
  ...prevState,
120
- ...createState(true, true),
86
+ ...createState(true, false),
87
+ }));
88
+
89
+ delayedPending = null;
90
+ }, delay);
91
+ }
92
+
93
+ return value
94
+ .then((resolvedValue) => {
95
+ if (delayedPending !== null) clearTimeout(delayedPending);
96
+
97
+ setState((prevState) => ({
98
+ ...prevState,
99
+ ...createState(true, true),
100
+ }));
101
+
102
+ return resolvedValue;
103
+ })
104
+ .catch((error) => {
105
+ if (delayedPending !== null) clearTimeout(delayedPending);
106
+
107
+ setState((prevState) => ({
108
+ ...prevState,
109
+ ...createState(true, true, error),
121
110
  }));
122
111
 
123
- return value;
124
- },
125
- [setState],
126
- );
112
+ if (options?.throws) throw error;
113
+ }) as T;
114
+ }
115
+
116
+ setState((prevState) => ({
117
+ ...prevState,
118
+ ...createState(true, true),
119
+ }));
120
+
121
+ return value;
122
+ },
123
+ [setState],
124
+ );
127
125
 
128
- return [state, withState, setState];
126
+ return [state, withState, setState];
129
127
  }
package/tsconfig.json CHANGED
@@ -1,13 +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
- }
1
+ {
2
+ "include": ["index.ts", "playwright.config.ts", "src", "tests"],
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
+ }