-
Notifications
You must be signed in to change notification settings - Fork 92
/
useResizeObserver.ts
109 lines (89 loc) · 3.25 KB
/
useResizeObserver.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
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
import { RefObject, useEffect } from 'react';
import { useSyncedRef } from '..';
import { isBrowser } from '../util/const';
export type IUseResizeObserverCallback = (entry: ResizeObserverEntry) => void;
interface IResizeObserverSingleton {
observer: ResizeObserver;
subscribe: (target: Element, callback: IUseResizeObserverCallback) => void;
unsubscribe: (target: Element, callback: IUseResizeObserverCallback) => void;
}
let observerSingleton: IResizeObserverSingleton;
function getResizeObserver(): IResizeObserverSingleton | undefined {
if (!isBrowser) return undefined;
if (observerSingleton) return observerSingleton;
const callbacks = new Map<Element, Set<IUseResizeObserverCallback>>();
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) =>
callbacks.get(entry.target)?.forEach((cb) => setTimeout(() => cb(entry), 0))
);
});
observerSingleton = {
observer,
subscribe: (target, callback) => {
let cbs = callbacks.get(target);
if (!cbs) {
// if target has no observers yet - register it
cbs = new Set<IUseResizeObserverCallback>();
callbacks.set(target, cbs);
observer.observe(target);
}
// as Set is duplicate-safe - simply add callback on each call
cbs.add(callback);
},
unsubscribe: (target, callback) => {
const cbs = callbacks.get(target);
// else branch should never occur in case of normal execution
// because callbacks map is hidden in closure - it is impossible to
// simulate situation with non-existent `cbs` Set
/* istanbul ignore else */
if (cbs) {
// remove current observer
cbs.delete(callback);
if (!cbs.size) {
// if no observers left unregister target completely
callbacks.delete(target);
observer.unobserve(target);
}
}
},
};
return observerSingleton;
}
/**
* Invokes a callback whenever ResizeObserver detects a change to target's size.
*
* @param target React reference or Element to track.
* @param callback Callback that will be invoked on resize.
* @param enabled Whether resize observer is enabled or not.
*/
export function useResizeObserver<T extends Element>(
target: RefObject<T> | T | null,
callback: IUseResizeObserverCallback,
enabled = true
): void {
const ro = enabled && getResizeObserver();
const cb = useSyncedRef(callback);
useEffect(() => {
if (!ro) return;
// as unsubscription in internals of our ResizeObserver abstraction can
// happen a bit later than effect cleanup invocation - we need a marker,
// that this handler should not be invoked anymore
let subscribed = true;
const tgt = target && 'current' in target ? target.current : target;
if (!tgt) return;
const handler: IUseResizeObserverCallback = (...args) => {
// it is reinsurance for the highly asynchronous invocations, almost
// impossible to achieve in tests, thus excluding from LOC
/* istanbul ignore else */
if (subscribed) {
cb.current(...args);
}
};
ro.subscribe(tgt, handler);
return () => {
subscribed = false;
ro.unsubscribe(tgt, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, ro]);
}