@pluv/react 0.38.13 → 0.39.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +54 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +209 -55
- package/dist/index.mjs +204 -47
- package/eslint.config.mjs +4 -0
- package/package.json +9 -9
- package/src/createBundle.tsx +63 -15
- package/src/internal/hooks/index.ts +2 -0
- package/src/internal/hooks/useAsyncQueue.ts +18 -0
- package/src/internal/hooks/useDeepAsyncMemo.ts +120 -0
- package/src/internal/index.ts +1 -1
- package/.eslintrc.js +0 -4
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import fastDeepEqual from "fast-deep-equal";
|
|
3
|
+
|
|
4
|
+
export type UseDeepAsyncMemoResult<T extends unknown> =
|
|
5
|
+
| {
|
|
6
|
+
error: null;
|
|
7
|
+
isInitialized: true;
|
|
8
|
+
isLoading: false;
|
|
9
|
+
value: T;
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
error: null;
|
|
13
|
+
isInitialized: boolean;
|
|
14
|
+
value: undefined;
|
|
15
|
+
isLoading: true;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
error: Error;
|
|
19
|
+
isInitialized: boolean;
|
|
20
|
+
value: undefined;
|
|
21
|
+
isLoading: false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const useDeepAsyncMemo = <T>(
|
|
25
|
+
factory: () => Promise<T>,
|
|
26
|
+
isEqual: (a: T, b: T) => boolean = fastDeepEqual,
|
|
27
|
+
): UseDeepAsyncMemoResult<T> => {
|
|
28
|
+
// Store the current async state (loading, value, error, etc.)
|
|
29
|
+
const [state, setState] = useState<UseDeepAsyncMemoResult<T>>({
|
|
30
|
+
isInitialized: false,
|
|
31
|
+
isLoading: true,
|
|
32
|
+
value: undefined,
|
|
33
|
+
error: null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Keep track of the last resolved value to compare with new ones
|
|
37
|
+
const lastValueRef = useRef<T | undefined>(undefined);
|
|
38
|
+
const hasValueRef = useRef(false); // Tracks whether we've ever resolved a value
|
|
39
|
+
|
|
40
|
+
// Tracks the current in-flight promise to guard against race conditions
|
|
41
|
+
const inflightRef = useRef<Promise<T> | null>(null);
|
|
42
|
+
|
|
43
|
+
// Used to prevent state updates after the component unmounts
|
|
44
|
+
const isMountedRef = useRef(true);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
isMountedRef.current = true;
|
|
48
|
+
|
|
49
|
+
// Trigger the async factory
|
|
50
|
+
const promise = factory();
|
|
51
|
+
inflightRef.current = promise;
|
|
52
|
+
|
|
53
|
+
// Set loading state
|
|
54
|
+
setState((prev) => {
|
|
55
|
+
return {
|
|
56
|
+
...prev,
|
|
57
|
+
isLoading: true,
|
|
58
|
+
error: null,
|
|
59
|
+
} as UseDeepAsyncMemoResult<T>;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
promise
|
|
63
|
+
.then((result) => {
|
|
64
|
+
// Ignore if component unmounted or if this promise is stale
|
|
65
|
+
if (!isMountedRef.current || inflightRef.current !== promise) return;
|
|
66
|
+
|
|
67
|
+
// Skip update if value hasn't changed
|
|
68
|
+
const isSame = hasValueRef.current && isEqual(lastValueRef.current as T, result);
|
|
69
|
+
if (isSame) {
|
|
70
|
+
setState((prev) => {
|
|
71
|
+
return {
|
|
72
|
+
...prev,
|
|
73
|
+
isInitialized: true,
|
|
74
|
+
isLoading: false,
|
|
75
|
+
} as UseDeepAsyncMemoResult<T>;
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Save the new value
|
|
81
|
+
lastValueRef.current = result;
|
|
82
|
+
hasValueRef.current = true;
|
|
83
|
+
|
|
84
|
+
setState({
|
|
85
|
+
isInitialized: true,
|
|
86
|
+
isLoading: false,
|
|
87
|
+
value: result,
|
|
88
|
+
error: null,
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
// Ignore if component unmounted or if this promise is stale
|
|
93
|
+
if (!isMountedRef.current || inflightRef.current !== promise) return;
|
|
94
|
+
|
|
95
|
+
setState({
|
|
96
|
+
isInitialized: hasValueRef.current,
|
|
97
|
+
isLoading: false,
|
|
98
|
+
value: undefined,
|
|
99
|
+
error: err instanceof Error ? err : new Error("Unknown error"),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Cleanup: prevent state updates after unmount
|
|
104
|
+
return () => {
|
|
105
|
+
isMountedRef.current = false;
|
|
106
|
+
};
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, [
|
|
109
|
+
/**
|
|
110
|
+
* !HACK
|
|
111
|
+
* @description We're only going to use the `isEqual` function to determine if this hook
|
|
112
|
+
* should re-run. This is basically our hook's version of a dependency array
|
|
113
|
+
* @date April 13, 2025
|
|
114
|
+
*/
|
|
115
|
+
// factory,
|
|
116
|
+
isEqual,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return state;
|
|
120
|
+
};
|
package/src/internal/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { useRerender, useSyncExternalStoreWithSelector } from "./hooks";
|
|
1
|
+
export { useAsyncQueue, useDeepAsyncMemo, useRerender, useSyncExternalStoreWithSelector } from "./hooks";
|
|
2
2
|
export { identity, shallowArrayEqual } from "./utils";
|
package/.eslintrc.js
DELETED