-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathuseDisposableMemo.ts
More file actions
111 lines (105 loc) · 3.29 KB
/
useDisposableMemo.ts
File metadata and controls
111 lines (105 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { useRef, useEffect, type DependencyList } from 'react';
const UNINITIALIZED = Symbol('UNINITIALIZED');
function depsEqual(a: DependencyList, b: DependencyList): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!Object.is(a[i], b[i])) return false;
}
return true;
}
/**
* Like `useMemo`, but with a cleanup callback for disposable native resources.
*
* - Value is created synchronously during render (available on first render).
* - When deps change, the old value is cleaned up during render and a new one
* is created.
* - On unmount in production: cleaned up synchronously in effect cleanup.
* - On unmount in development: cleanup is deferred via `setTimeout(0)` so that
* fast refresh and Strict Mode can cancel it when effects re-run.
*
* Replaces the common `useMemo` + dispose-in-`useEffect`-cleanup pattern that
* breaks on fast refresh (HMR re-runs all effect cleanups, disposing the native
* object, but `useMemo` returns the same dead reference):
*
* ```tsx
* // BEFORE — breaks on fast refresh
* const property = useMemo(() => instance?.getProperty(path), [instance, path]);
* useEffect(() => {
* const unsub = property?.addListener(setValue);
* return () => { unsub?.(); property?.dispose(); };
* }, [property]);
*
* // AFTER
* const property = useDisposableMemo(
* () => instance?.getProperty(path),
* (p) => p?.dispose(),
* [instance, path]
* );
* useEffect(() => {
* const unsub = property?.addListener(setValue);
* return () => unsub?.(); // only unsubscribe, no dispose
* }, [property]);
* ```
*/
export function useDisposableMemo<T>(
factory: () => T,
cleanup: (value: T) => void,
deps: DependencyList
): T {
const ref = useRef<{
value: T;
deps: DependencyList | typeof UNINITIALIZED;
pendingDisposal: ReturnType<typeof setTimeout> | null;
}>({
value: undefined as T,
deps: UNINITIALIZED,
pendingDisposal: null,
});
const cleanupRef = useRef(cleanup);
cleanupRef.current = cleanup;
if (
ref.current.deps === UNINITIALIZED ||
!depsEqual(ref.current.deps, deps)
) {
if (__DEV__ && ref.current.pendingDisposal !== null) {
clearTimeout(ref.current.pendingDisposal);
ref.current.pendingDisposal = null;
}
if (ref.current.deps !== UNINITIALIZED) {
try {
cleanupRef.current(ref.current.value);
} catch {
// Swallow cleanup errors — the old value is being replaced regardless.
}
}
ref.current = { value: factory(), deps, pendingDisposal: null };
}
useEffect(() => {
if (__DEV__) {
if (ref.current.pendingDisposal !== null) {
clearTimeout(ref.current.pendingDisposal);
ref.current.pendingDisposal = null;
}
}
return () => {
if (__DEV__) {
const val = ref.current.value;
ref.current.pendingDisposal = setTimeout(() => {
try {
cleanupRef.current(val);
} catch {
// Swallow — object may already be in a bad state.
}
ref.current.pendingDisposal = null;
}, 0);
} else {
try {
cleanupRef.current(ref.current.value);
} catch {
// Swallow — object may already be in a bad state.
}
}
};
}, []);
return ref.current.value;
}