Bin
2025-12-17 1d710f844b65d9bfdf986a71a3b924cd70598a41
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { useCallback, useEffect, useRef, useState } from "react";
 
export interface ResizeObserverSize {
  width?: number;
  height?: number;
}
 
export interface UseResizeObserverOptions<T extends ResizeObserverSize = ResizeObserverSize> {
  /**
   * Function to extract size data from ResizeObserver entries
   * Default: returns the size of the first element
   */
  extractSize?: (elements: Element[], entries: ResizeObserverEntry[]) => T;
  /**
   * Debounce time in milliseconds to prevent excessive updates
   * Default: 16ms (roughly 60fps)
   */
  debounceMs?: number;
  defaultSize?: T;
}
 
export type ElementsOption = Element | Element[];
 
const DEFAULT_DEBOUNCE_MS = 16;
 
const defaultExtractSizeOneElement = <T extends ResizeObserverSize = ResizeObserverSize>(
  _elements: Element[],
  entries: ResizeObserverEntry[],
): T => {
  const { width, height } = entries[0].contentRect;
  return { width, height } as T;
};
 
const defaultExtractSizeMultipleElements = <T extends ResizeObserverSize = ResizeObserverSize>(
  elements: Element[],
  entries: ResizeObserverEntry[],
): T => {
  if (entries.length === 0) return {} as T;
  const { width, height } = elements[0].getBoundingClientRect();
  return { width, height } as T;
};
 
/**
 * Hook for observing resize changes on elements
 * Prevents "ResizeObserver loop limit exceeded" errors through debouncing
 *
 * @param elements - Element or array of elements to observe
 * @param options - Configuration options
 * @returns Current size data that triggers re-renders
 */
export function useResizeObserver<T extends ResizeObserverSize = ResizeObserverSize>(
  elements: ElementsOption,
  options: UseResizeObserverOptions<T> = {},
): T {
  const elementsArray = Array.isArray(elements) ? elements : [elements];
  const {
    extractSize = elementsArray.length === 1 ? defaultExtractSizeOneElement : defaultExtractSizeMultipleElements,
    debounceMs = DEFAULT_DEBOUNCE_MS,
  } = options;
 
  const [size, setSize] = useState<T>({} as T);
  const observerRef = useRef<ResizeObserver | null>(null);
  const timeoutRef = useRef<number | null>(null);
  const rafRef = useRef<number | null>(null);
  const elementsRef = useRef<Element[] | null>(null);
 
  // Memoized callback to handle resize events
  const handleResize = useCallback(
    (entries: ResizeObserverEntry[]) => {
      // Clear any existing timeout and animation frame to debounce rapid changes
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
      }
 
      // Use requestAnimationFrame + setTimeout to prevent loop limit exceeded errors
      timeoutRef.current = window.setTimeout(() => {
        rafRef.current = requestAnimationFrame(() => {
          try {
            const newSize = extractSize(elementsRef.current as Element[], entries);
            setSize((prevSize) => {
              if (prevSize === newSize) return prevSize;
              if (JSON.stringify(prevSize) !== JSON.stringify(newSize)) {
                return newSize;
              }
              return prevSize;
            });
          } catch (error) {
            console.warn("Error in resize observer callback: ", error);
          }
          rafRef.current = null;
        });
        timeoutRef.current = null;
      }, debounceMs);
    },
    [extractSize, debounceMs],
  );
 
  // Effect to set up and clean up the ResizeObserver
  useEffect(() => {
    const elementsArray = Array.isArray(elements) ? elements : [elements];
 
    if (elementsArray.length === 0) {
      setSize({} as T);
      return;
    }
 
    // Create new ResizeObserver
    observerRef.current = new ResizeObserver(handleResize);
 
    // Observe all elements
    elementsArray.forEach((element) => {
      observerRef.current?.observe(element);
    });
 
    // Store current elements reference for cleanup comparison
    elementsRef.current = elementsArray;
 
    // Cleanup function
    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
        observerRef.current = null;
      }
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      }
    };
  }, [elements, handleResize]);
 
  return size ?? ({} as T);
}