-
-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathuseResolvedElement.ts
77 lines (69 loc) · 2.49 KB
/
useResolvedElement.ts
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
import { RefCallback, RefObject, useCallback, useEffect, useRef } from "react";
type SubscriberCleanupFunction = () => void;
type SubscriberResponse = SubscriberCleanupFunction | void;
// This could've been more streamlined with internal state instead of abusing
// refs to such extent, but then composing hooks and components could not opt out of unnecessary renders.
export default function useResolvedElement<T extends Element>(
subscriber: (element: T) => SubscriberResponse,
host: Window & typeof globalThis = window,
refOrElement?: T | RefObject<T> | null,
): RefCallback<T> {
const lastReportRef = useRef<{
element: T | null;
subscriber: typeof subscriber;
cleanup?: SubscriberResponse;
} | null>(null);
const refOrElementRef = useRef<typeof refOrElement>(null);
refOrElementRef.current = refOrElement;
const cbElementRef = useRef<T | null>(null);
// Calling re-evaluation after each render without using a dep array,
// as the ref object's current value could've changed since the last render.
useEffect(() => {
evaluateSubscription();
});
const evaluateSubscription = useCallback(() => {
const cbElement = cbElementRef.current;
const refOrElement = refOrElementRef.current;
// Ugly ternary. But smaller than an if-else block.
const element: T | null = cbElement
? cbElement
: refOrElement
? refOrElement instanceof host.Element
? refOrElement
: refOrElement.current
: null;
if (
lastReportRef.current &&
lastReportRef.current.element === element &&
lastReportRef.current.subscriber === subscriber
) {
return;
}
if (lastReportRef.current && lastReportRef.current.cleanup) {
lastReportRef.current.cleanup();
}
lastReportRef.current = {
element,
subscriber,
// Only calling the subscriber, if there's an actual element to report.
// Setting cleanup to undefined unless a subscriber returns one, as an existing cleanup function would've been just called.
cleanup: element ? subscriber(element) : undefined,
};
}, [subscriber]);
// making sure we call the cleanup function on unmount
useEffect(() => {
return () => {
if (lastReportRef.current && lastReportRef.current.cleanup) {
lastReportRef.current.cleanup();
lastReportRef.current = null;
}
};
}, []);
return useCallback(
(element) => {
cbElementRef.current = element;
evaluateSubscription();
},
[evaluateSubscription]
);
}