@pluv/react 0.38.14 → 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.
@@ -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
+ };
@@ -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
@@ -1,4 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- extends: ["pluv"],
4
- };