React 19 introduces cleanup functions in ref callbacks. When you return a function from a ref callback, React calls it when the component unmounts or when the ref changes (the element is replaced).
Before React 19 — cleanup required useEffect:
1function TrackedElement() {2 const ref = useRef<HTMLDivElement>(null);34 useEffect(() => {5 const node = ref.current;6 if (!node) return;78 // Setup9 const observer = new ResizeObserver(handleResize);10 observer.observe(node);1112 // Cleanup13 return () => observer.disconnect();14 }, []);1516 return <div ref={ref}>Content</div>;17}
After React 19 — cleanup colocated with ref:
1function TrackedElement() {2 return (3 <div4 ref={(node) => {5 if (node) {6 // Setup: observe element7 const observer = new ResizeObserver(handleResize);8 observer.observe(node);910 // Cleanup: stop observing (React 19)11 return () => observer.disconnect();12 }13 }}14 >15 Content16 </div>17 );18}
When cleanup runs:
Practical examples:
ResizeObserver:
1function ResizableBox() {2 return (3 <div4 ref={(node) => {5 if (node) {6 const observer = new ResizeObserver(entries => {7 console.log("Size:", entries[0].contentRect);8 });9 observer.observe(node);10 return () => observer.disconnect();11 }12 }}13 >14 Resize me15 </div>16 );17}
IntersectionObserver:
1function VisibleTracker() {2 return (3 <div4 ref={(node) => {5 if (node) {6 const observer = new IntersectionObserver(entries => {7 if (entries[0].isIntersecting) {8 console.log("Element visible!");9 }10 });11 observer.observe(node);12 return () => observer.disconnect();13 }14 }}15 >16 Track me17 </div>18 );19}
Benefits:
useRef + useEffect patterns