Back to Blog
ReactRefsCallbacksPatterns

React Ref Callback Pattern

Master React ref callbacks for dynamic refs, measuring elements, and complex ref scenarios.

B
Bootspring Team
Engineering
January 30, 2020
6 min read

Ref callbacks provide more control than useRef for dynamic scenarios. Here's how to use them.

Basic Ref Callback#

1import { useCallback, useState } from 'react'; 2 3function MeasuredComponent() { 4 const [height, setHeight] = useState(0); 5 6 // Ref callback receives the DOM element 7 const measuredRef = useCallback((node: HTMLDivElement | null) => { 8 if (node !== null) { 9 setHeight(node.getBoundingClientRect().height); 10 } 11 }, []); 12 13 return ( 14 <div ref={measuredRef}> 15 <p>This element is {height}px tall</p> 16 <p>More content here...</p> 17 </div> 18 ); 19} 20 21// Without useCallback (simpler but recreates on each render) 22function SimpleRef() { 23 const [width, setWidth] = useState(0); 24 25 return ( 26 <div ref={(node) => { 27 if (node) { 28 setWidth(node.offsetWidth); 29 } 30 }}> 31 Width: {width}px 32 </div> 33 ); 34}

Dynamic Element Lists#

1import { useRef, useCallback } from 'react'; 2 3function DynamicList({ items }) { 4 // Store refs for multiple elements 5 const itemRefs = useRef<Map<string, HTMLLIElement>>(new Map()); 6 7 const setItemRef = useCallback((id: string) => { 8 return (node: HTMLLIElement | null) => { 9 if (node) { 10 itemRefs.current.set(id, node); 11 } else { 12 itemRefs.current.delete(id); 13 } 14 }; 15 }, []); 16 17 const scrollToItem = (id: string) => { 18 const node = itemRefs.current.get(id); 19 node?.scrollIntoView({ behavior: 'smooth' }); 20 }; 21 22 return ( 23 <ul> 24 {items.map((item) => ( 25 <li key={item.id} ref={setItemRef(item.id)}> 26 {item.name} 27 </li> 28 ))} 29 </ul> 30 ); 31}

Focus Management#

1function AutoFocusInput() { 2 const focusRef = useCallback((node: HTMLInputElement | null) => { 3 if (node) { 4 node.focus(); 5 node.select(); // Also select text 6 } 7 }, []); 8 9 return <input ref={focusRef} defaultValue="Edit me" />; 10} 11 12// Focus first invalid field 13function Form() { 14 const firstErrorRef = useCallback((node: HTMLInputElement | null) => { 15 if (node) { 16 node.focus(); 17 node.scrollIntoView({ behavior: 'smooth', block: 'center' }); 18 } 19 }, []); 20 21 return ( 22 <form> 23 <input name="name" /> 24 <input 25 name="email" 26 ref={hasError ? firstErrorRef : undefined} 27 /> 28 <input name="phone" /> 29 </form> 30 ); 31}

Intersection Observer#

1import { useCallback, useState } from 'react'; 2 3function LazyImage({ src, alt }) { 4 const [isVisible, setIsVisible] = useState(false); 5 6 const imageRef = useCallback((node: HTMLImageElement | null) => { 7 if (!node) return; 8 9 const observer = new IntersectionObserver( 10 ([entry]) => { 11 if (entry.isIntersecting) { 12 setIsVisible(true); 13 observer.disconnect(); 14 } 15 }, 16 { rootMargin: '100px' } 17 ); 18 19 observer.observe(node); 20 21 // Cleanup handled by disconnect on intersection 22 }, []); 23 24 return ( 25 <img 26 ref={imageRef} 27 src={isVisible ? src : '/placeholder.png'} 28 alt={alt} 29 /> 30 ); 31} 32 33// With cleanup using useEffect 34function ObservedElement({ onVisible }) { 35 const [element, setElement] = useState<HTMLDivElement | null>(null); 36 37 useEffect(() => { 38 if (!element) return; 39 40 const observer = new IntersectionObserver(([entry]) => { 41 if (entry.isIntersecting) { 42 onVisible(); 43 } 44 }); 45 46 observer.observe(element); 47 48 return () => observer.disconnect(); 49 }, [element, onVisible]); 50 51 return <div ref={setElement}>Observed content</div>; 52}

ResizeObserver#

1import { useCallback, useState, useEffect } from 'react'; 2 3function ResizableElement() { 4 const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 5 const [element, setElement] = useState<HTMLDivElement | null>(null); 6 7 useEffect(() => { 8 if (!element) return; 9 10 const observer = new ResizeObserver((entries) => { 11 const { width, height } = entries[0].contentRect; 12 setDimensions({ width, height }); 13 }); 14 15 observer.observe(element); 16 17 return () => observer.disconnect(); 18 }, [element]); 19 20 return ( 21 <div ref={setElement} style={{ resize: 'both', overflow: 'auto' }}> 22 <p>Width: {Math.round(dimensions.width)}px</p> 23 <p>Height: {Math.round(dimensions.height)}px</p> 24 </div> 25 ); 26} 27 28// Custom hook 29function useResizeObserver<T extends HTMLElement>() { 30 const [element, setElement] = useState<T | null>(null); 31 const [size, setSize] = useState({ width: 0, height: 0 }); 32 33 useEffect(() => { 34 if (!element) return; 35 36 const observer = new ResizeObserver((entries) => { 37 const { width, height } = entries[0].contentRect; 38 setSize({ width, height }); 39 }); 40 41 observer.observe(element); 42 return () => observer.disconnect(); 43 }, [element]); 44 45 return [setElement, size] as const; 46} 47 48// Usage 49function Component() { 50 const [ref, { width, height }] = useResizeObserver<HTMLDivElement>(); 51 52 return ( 53 <div ref={ref}> 54 {width} x {height} 55 </div> 56 ); 57}

Animation Trigger#

1import { useCallback } from 'react'; 2 3function AnimatedElement() { 4 const animateRef = useCallback((node: HTMLDivElement | null) => { 5 if (!node) return; 6 7 // Trigger CSS animation 8 node.classList.add('animate-in'); 9 10 // Or use Web Animations API 11 node.animate( 12 [ 13 { opacity: 0, transform: 'translateY(20px)' }, 14 { opacity: 1, transform: 'translateY(0)' }, 15 ], 16 { 17 duration: 300, 18 easing: 'ease-out', 19 fill: 'forwards', 20 } 21 ); 22 }, []); 23 24 return ( 25 <div ref={animateRef} className="card"> 26 Animated content 27 </div> 28 ); 29} 30 31// Staggered animation for list 32function AnimatedList({ items }) { 33 const createAnimatedRef = useCallback((index: number) => { 34 return (node: HTMLLIElement | null) => { 35 if (!node) return; 36 37 node.animate( 38 [ 39 { opacity: 0, transform: 'translateX(-20px)' }, 40 { opacity: 1, transform: 'translateX(0)' }, 41 ], 42 { 43 duration: 300, 44 delay: index * 50, // Stagger 45 easing: 'ease-out', 46 fill: 'forwards', 47 } 48 ); 49 }; 50 }, []); 51 52 return ( 53 <ul> 54 {items.map((item, index) => ( 55 <li key={item.id} ref={createAnimatedRef(index)}> 56 {item.name} 57 </li> 58 ))} 59 </ul> 60 ); 61}

Third-Party Libraries#

1import { useCallback, useEffect, useRef } from 'react'; 2 3function ChartComponent({ data }) { 4 const chartInstance = useRef<Chart | null>(null); 5 6 const chartRef = useCallback((canvas: HTMLCanvasElement | null) => { 7 if (!canvas) { 8 // Cleanup on unmount 9 chartInstance.current?.destroy(); 10 chartInstance.current = null; 11 return; 12 } 13 14 // Initialize chart library 15 chartInstance.current = new Chart(canvas, { 16 type: 'line', 17 data: data, 18 }); 19 }, [data]); 20 21 return <canvas ref={chartRef} />; 22} 23 24// Video player 25function VideoPlayer({ src }) { 26 const playerRef = useCallback((video: HTMLVideoElement | null) => { 27 if (!video) return; 28 29 // Initialize video player library 30 const player = new VideoJS(video, { 31 controls: true, 32 autoplay: false, 33 }); 34 35 return () => player.dispose(); 36 }, []); 37 38 return ( 39 <video ref={playerRef} className="video-js"> 40 <source src={src} type="video/mp4" /> 41 </video> 42 ); 43}

Merging Refs#

1import { useCallback, useRef, forwardRef } from 'react'; 2 3// Merge multiple refs 4function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) { 5 return (node: T | null) => { 6 refs.forEach((ref) => { 7 if (typeof ref === 'function') { 8 ref(node); 9 } else if (ref) { 10 (ref as React.MutableRefObject<T | null>).current = node; 11 } 12 }); 13 }; 14} 15 16// Component with internal and forwarded ref 17const Input = forwardRef<HTMLInputElement, InputProps>((props, forwardedRef) => { 18 const internalRef = useRef<HTMLInputElement>(null); 19 20 const measureRef = useCallback((node: HTMLInputElement | null) => { 21 if (node) { 22 console.log('Input width:', node.offsetWidth); 23 } 24 }, []); 25 26 return ( 27 <input 28 ref={mergeRefs(forwardedRef, internalRef, measureRef)} 29 {...props} 30 /> 31 ); 32});

Best Practices#

Usage: ✓ Use for measurement/observers ✓ Use for dynamic element lists ✓ Use for third-party integrations ✓ Wrap in useCallback when needed Cleanup: ✓ Handle null (element unmounted) ✓ Disconnect observers ✓ Dispose library instances ✓ Clear stored references Performance: ✓ Memoize with useCallback ✓ Avoid creating refs in loops ✓ Use Map for dynamic refs ✓ Batch state updates Avoid: ✗ Side effects without cleanup ✗ Recreating refs unnecessarily ✗ Forgetting null checks ✗ Complex logic in callbacks

Conclusion#

Ref callbacks provide fine-grained control for measuring elements, managing observers, and integrating third-party libraries. Use them when useRef isn't flexible enough, especially for dynamic lists and elements that need setup/cleanup logic. Always handle the null case for proper cleanup.

Share this article

Help spread the word about Bootspring