diff --git a/packages/@react-aria/utils/src/useResizeObserver.ts b/packages/@react-aria/utils/src/useResizeObserver.ts index bb328f26358..e2cb9530ec2 100644 --- a/packages/@react-aria/utils/src/useResizeObserver.ts +++ b/packages/@react-aria/utils/src/useResizeObserver.ts @@ -1,4 +1,4 @@ -import {RefObject, useEffect} from 'react'; +import {RefObject, useEffect, useRef} from 'react'; function hasResizeObserver() { return typeof window.ResizeObserver !== 'undefined'; @@ -11,6 +11,7 @@ type useResizeObserverOptionsType = { export function useResizeObserver(options: useResizeObserverOptionsType) { const {ref, onResize} = options; + let raf = useRef(null); useEffect(() => { let element = ref?.current; @@ -24,17 +25,28 @@ export function useResizeObserver(options: useResizeObserverO window.removeEventListener('resize', onResize, false); }; } else { - const resizeObserverInstance = new window.ResizeObserver((entries) => { - if (!entries.length) { + if (raf.current) { return; } - - onResize(); + // avoid Error - ResizeObserver loop limit exceeded + // it's ok to use a raf, ResizeObservers are already async and now we're just debouncing on frames + raf.current = window.requestAnimationFrame(() => { + raf.current = null; + if (!Array.isArray(entries) || !entries.length) { + return; + } + onResize(); + }); }); + resizeObserverInstance.observe(element); return () => { + if (raf.current) { + window.cancelAnimationFrame(raf.current); + raf.current = null; + } if (element) { resizeObserverInstance.unobserve(element); } diff --git a/packages/@react-aria/utils/stories/useResizeObserver.stories.tsx b/packages/@react-aria/utils/stories/useResizeObserver.stories.tsx new file mode 100644 index 00000000000..7a4a335d77c --- /dev/null +++ b/packages/@react-aria/utils/stories/useResizeObserver.stories.tsx @@ -0,0 +1,55 @@ + +import {Button} from '@react-spectrum/button'; +import {Meta, Story} from '@storybook/react'; +import React, {useEffect, useRef, useState} from 'react'; +import {useResizeObserver} from '../src'; + +export default { + title: 'useResizeObserver' +} as Meta; + +const Template: Story = () => ( + +); + +export const UseResizeObserverLoopLimit = Template.bind({}); +UseResizeObserverLoopLimit.args = {}; + +const animalSet = ['🐰', '🦊', '🐻', '🐭', '🐼', '🐸']; +function App() { + let ref = useRef(); + let index = useRef(0); + let [animals, setAnimals] = useState([animalSet[0]]); + + function insertAnimal() { + index.current = index.current + 1; + setAnimals(prev => [...prev, animalSet[index.current % animalSet.length]]); + } + + useEffect(() => { + let onError = (err) => { + console.log(err); + }; + window.addEventListener('error', onError); + return () => { + window.removeEventListener('error', onError); + }; + }, []); + + let onResize = () => { + const {width} = ref.current.getBoundingClientRect(); + + ref.current.style.height = `${width}px`; + }; + + useResizeObserver({onResize, ref}); + + return ( + <> +
+ {animals} +
+ + + ); +}