Open History to see the last commit
1import { flushSync } from "react-dom";
2
3type ResizeCallback = (entries: readonly ResizeObserverEntry[]) => void;
4
5interface Subscription {
6 readonly callback: ResizeCallback;
7 active: boolean;
8}
9
10interface Target {
11 readonly subscriptions: Set<Subscription>;
12 waiting: Set<Subscription> | null;
13 last: ResizeObserverEntry | null;
14}
15
16let observer: ResizeObserver | null = null;
17const targets = new Map<Element, Target>();
18
19function deliver(entries: readonly ResizeObserverEntry[]) {
20 const batches = new Map<Subscription, ResizeObserverEntry[]>();
21 for (const entry of entries) {
22 const target = targets.get(entry.target);
23 if (!target) continue;
24 let recipients = target.subscriptions;
25 if (target.waiting) {
26 // Re-observing for a new subscriber redelivers an unchanged size to everyone else too.
27 const previous = target.last?.contentRect;
28 if (
29 previous?.width === entry.contentRect.width &&
30 previous.height === entry.contentRect.height
31 ) {
32 recipients = target.waiting;
33 }
34 target.waiting = null;
35 }
36 target.last = entry;
37 for (const subscription of recipients) {
38 const batch = batches.get(subscription);
39 if (batch) batch.push(entry);
40 else batches.set(subscription, [entry]);
41 }
42 }
43 if (batches.size === 0) return;
44 flushSync(() => {
45 for (const [subscription, batch] of batches) {
46 if (!subscription.active) continue;
47 try {
48 subscription.callback(batch);
49 } catch (error) {
50 queueMicrotask(() => {
51 throw error;
52 });
53 }
54 }
55 });
56}
57
58/** Flushes callbacks together before paint; ordinary React updates would leave a stale frame. */
59export function observeResize(
60 elements: Element | readonly Element[],
61 callback: ResizeCallback,
62): () => void {
63 if (typeof ResizeObserver === "undefined") return () => {};
64 const shared = (observer ??= new ResizeObserver(deliver));
65 const subscription: Subscription = { callback, active: true };
66 const observed: readonly Element[] = Array.isArray(elements) ? elements : [elements as Element];
67 for (const element of observed) {
68 const target = targets.get(element);
69 if (!target) {
70 targets.set(element, { subscriptions: new Set([subscription]), waiting: null, last: null });
71 shared.observe(element);
72 continue;
73 }
74 target.subscriptions.add(subscription);
75 if (!target.last) continue;
76 (target.waiting ??= new Set()).add(subscription);
77 shared.unobserve(element);
78 shared.observe(element);
79 }
80 return () => {
81 if (!subscription.active) return;
82 subscription.active = false;
83 const unobserved = observed.filter((element) => {
84 const target = targets.get(element);
85 if (!target?.subscriptions.delete(subscription)) return false;
86 target.waiting?.delete(subscription);
87 return target.subscriptions.size === 0 && targets.delete(element);
88 });
89 if (targets.size > 0) {
90 for (const element of unobserved) shared.unobserve(element);
91 } else {
92 observer?.disconnect();
93 observer = null;
94 }
95 };
96}