Back to Blog
ReactRefsDOMPatterns

React Ref Patterns

Master refs in React. From DOM access to forwarding refs to imperative handles and ref callbacks.

B
Bootspring Team
Engineering
February 21, 2021
6 min read

Refs provide direct access to DOM elements and persist values across renders. Here's how to use them.

Basic useRef#

1import { useRef, useEffect } from 'react'; 2 3function TextInput() { 4 const inputRef = useRef<HTMLInputElement>(null); 5 6 useEffect(() => { 7 // Focus input on mount 8 inputRef.current?.focus(); 9 }, []); 10 11 return <input ref={inputRef} type="text" />; 12} 13 14// Storing mutable values 15function Timer() { 16 const intervalRef = useRef<number | null>(null); 17 const [count, setCount] = useState(0); 18 19 useEffect(() => { 20 intervalRef.current = window.setInterval(() => { 21 setCount(c => c + 1); 22 }, 1000); 23 24 return () => { 25 if (intervalRef.current) { 26 clearInterval(intervalRef.current); 27 } 28 }; 29 }, []); 30 31 const stop = () => { 32 if (intervalRef.current) { 33 clearInterval(intervalRef.current); 34 } 35 }; 36 37 return ( 38 <div> 39 <p>Count: {count}</p> 40 <button onClick={stop}>Stop</button> 41 </div> 42 ); 43}

forwardRef#

1import { forwardRef, useRef } from 'react'; 2 3// Forward ref to inner element 4const FancyInput = forwardRef<HTMLInputElement, { label: string }>( 5 function FancyInput({ label }, ref) { 6 return ( 7 <div className="fancy-input"> 8 <label>{label}</label> 9 <input ref={ref} type="text" /> 10 </div> 11 ); 12 } 13); 14 15// Usage 16function Form() { 17 const inputRef = useRef<HTMLInputElement>(null); 18 19 const focus = () => { 20 inputRef.current?.focus(); 21 }; 22 23 return ( 24 <div> 25 <FancyInput ref={inputRef} label="Name" /> 26 <button onClick={focus}>Focus</button> 27 </div> 28 ); 29} 30 31// Forward ref with generic types 32interface InputProps<T> { 33 value: T; 34 onChange: (value: T) => void; 35} 36 37const GenericInput = forwardRef(function GenericInput<T>( 38 props: InputProps<T>, 39 ref: React.ForwardedRef<HTMLInputElement> 40) { 41 return <input ref={ref} />; 42});

useImperativeHandle#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3interface ModalHandle { 4 open: () => void; 5 close: () => void; 6} 7 8const Modal = forwardRef<ModalHandle, { children: React.ReactNode }>( 9 function Modal({ children }, ref) { 10 const [isOpen, setIsOpen] = useState(false); 11 12 useImperativeHandle(ref, () => ({ 13 open: () => setIsOpen(true), 14 close: () => setIsOpen(false), 15 }), []); 16 17 if (!isOpen) return null; 18 19 return ( 20 <div className="modal"> 21 {children} 22 <button onClick={() => setIsOpen(false)}>Close</button> 23 </div> 24 ); 25 } 26); 27 28// Usage 29function App() { 30 const modalRef = useRef<ModalHandle>(null); 31 32 return ( 33 <div> 34 <button onClick={() => modalRef.current?.open()}> 35 Open Modal 36 </button> 37 <Modal ref={modalRef}> 38 <h2>Modal Content</h2> 39 </Modal> 40 </div> 41 ); 42} 43 44// Video player example 45interface VideoPlayerHandle { 46 play: () => void; 47 pause: () => void; 48 seek: (time: number) => void; 49 getCurrentTime: () => number; 50} 51 52const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>( 53 function VideoPlayer({ src }, ref) { 54 const videoRef = useRef<HTMLVideoElement>(null); 55 56 useImperativeHandle(ref, () => ({ 57 play: () => videoRef.current?.play(), 58 pause: () => videoRef.current?.pause(), 59 seek: (time) => { 60 if (videoRef.current) { 61 videoRef.current.currentTime = time; 62 } 63 }, 64 getCurrentTime: () => videoRef.current?.currentTime ?? 0, 65 }), []); 66 67 return <video ref={videoRef} src={src} />; 68 } 69);

Ref Callbacks#

1// Callback ref for dynamic refs 2function MeasureExample() { 3 const [height, setHeight] = useState(0); 4 5 const measuredRef = useCallback((node: HTMLDivElement | null) => { 6 if (node !== null) { 7 setHeight(node.getBoundingClientRect().height); 8 } 9 }, []); 10 11 return ( 12 <div> 13 <div ref={measuredRef}>Content to measure</div> 14 <p>Height: {height}px</p> 15 </div> 16 ); 17} 18 19// Multiple refs for list items 20function ListWithRefs() { 21 const itemRefs = useRef<Map<string, HTMLLIElement>>(new Map()); 22 23 const setItemRef = useCallback((id: string, node: HTMLLIElement | null) => { 24 if (node) { 25 itemRefs.current.set(id, node); 26 } else { 27 itemRefs.current.delete(id); 28 } 29 }, []); 30 31 const scrollToItem = (id: string) => { 32 const node = itemRefs.current.get(id); 33 node?.scrollIntoView({ behavior: 'smooth' }); 34 }; 35 36 return ( 37 <ul> 38 {items.map(item => ( 39 <li 40 key={item.id} 41 ref={(node) => setItemRef(item.id, node)} 42 > 43 {item.name} 44 </li> 45 ))} 46 </ul> 47 ); 48}

Combining Multiple Refs#

1// Merge multiple refs 2function useMergeRefs<T>(...refs: React.Ref<T>[]) { 3 return useCallback((node: T | null) => { 4 refs.forEach(ref => { 5 if (typeof ref === 'function') { 6 ref(node); 7 } else if (ref) { 8 (ref as React.MutableRefObject<T | null>).current = node; 9 } 10 }); 11 }, refs); 12} 13 14// Usage 15const Input = forwardRef<HTMLInputElement, InputProps>( 16 function Input(props, forwardedRef) { 17 const localRef = useRef<HTMLInputElement>(null); 18 const mergedRef = useMergeRefs(localRef, forwardedRef); 19 20 useEffect(() => { 21 // Can use localRef internally 22 localRef.current?.focus(); 23 }, []); 24 25 return <input ref={mergedRef} {...props} />; 26 } 27);

Ref with Intersection Observer#

1function useIntersectionObserver<T extends Element>( 2 options?: IntersectionObserverInit 3) { 4 const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null); 5 const elementRef = useRef<T | null>(null); 6 7 useEffect(() => { 8 const element = elementRef.current; 9 if (!element) return; 10 11 const observer = new IntersectionObserver( 12 ([entry]) => setEntry(entry), 13 options 14 ); 15 16 observer.observe(element); 17 return () => observer.disconnect(); 18 }, [options?.root, options?.rootMargin, options?.threshold]); 19 20 return { ref: elementRef, entry }; 21} 22 23// Usage 24function LazyImage({ src, alt }: { src: string; alt: string }) { 25 const { ref, entry } = useIntersectionObserver<HTMLImageElement>({ 26 rootMargin: '100px', 27 }); 28 29 const isVisible = entry?.isIntersecting ?? false; 30 31 return ( 32 <img 33 ref={ref} 34 src={isVisible ? src : undefined} 35 alt={alt} 36 loading="lazy" 37 /> 38 ); 39}

Previous Value Ref#

1function usePrevious<T>(value: T): T | undefined { 2 const ref = useRef<T>(); 3 4 useEffect(() => { 5 ref.current = value; 6 }, [value]); 7 8 return ref.current; 9} 10 11// Usage 12function Counter() { 13 const [count, setCount] = useState(0); 14 const prevCount = usePrevious(count); 15 16 return ( 17 <div> 18 <p>Current: {count}, Previous: {prevCount ?? 'N/A'}</p> 19 <button onClick={() => setCount(c => c + 1)}>Increment</button> 20 </div> 21 ); 22}

Latest Value Ref#

1// Keep ref in sync with latest value 2function useLatest<T>(value: T) { 3 const ref = useRef(value); 4 ref.current = value; 5 return ref; 6} 7 8// Usage in callbacks 9function Search({ onSearch }: { onSearch: (query: string) => void }) { 10 const [query, setQuery] = useState(''); 11 const onSearchRef = useLatest(onSearch); 12 13 useEffect(() => { 14 const timeoutId = setTimeout(() => { 15 onSearchRef.current(query); 16 }, 300); 17 18 return () => clearTimeout(timeoutId); 19 }, [query]); 20 21 return ( 22 <input 23 value={query} 24 onChange={e => setQuery(e.target.value)} 25 /> 26 ); 27}

DOM Manipulation#

1function ScrollContainer() { 2 const containerRef = useRef<HTMLDivElement>(null); 3 4 const scrollToTop = () => { 5 containerRef.current?.scrollTo({ 6 top: 0, 7 behavior: 'smooth', 8 }); 9 }; 10 11 const scrollToBottom = () => { 12 if (containerRef.current) { 13 containerRef.current.scrollTop = containerRef.current.scrollHeight; 14 } 15 }; 16 17 return ( 18 <div> 19 <div ref={containerRef} className="scroll-container"> 20 {/* Content */} 21 </div> 22 <button onClick={scrollToTop}>Scroll to Top</button> 23 <button onClick={scrollToBottom}>Scroll to Bottom</button> 24 </div> 25 ); 26} 27 28// Focus management 29function FocusManager() { 30 const firstRef = useRef<HTMLButtonElement>(null); 31 const lastRef = useRef<HTMLButtonElement>(null); 32 33 const handleKeyDown = (e: React.KeyboardEvent) => { 34 if (e.key === 'Tab') { 35 if (e.shiftKey && document.activeElement === firstRef.current) { 36 e.preventDefault(); 37 lastRef.current?.focus(); 38 } else if (!e.shiftKey && document.activeElement === lastRef.current) { 39 e.preventDefault(); 40 firstRef.current?.focus(); 41 } 42 } 43 }; 44 45 return ( 46 <div onKeyDown={handleKeyDown}> 47 <button ref={firstRef}>First</button> 48 <button>Middle</button> 49 <button ref={lastRef}>Last</button> 50 </div> 51 ); 52}

Canvas Ref#

1function Canvas() { 2 const canvasRef = useRef<HTMLCanvasElement>(null); 3 4 useEffect(() => { 5 const canvas = canvasRef.current; 6 if (!canvas) return; 7 8 const ctx = canvas.getContext('2d'); 9 if (!ctx) return; 10 11 // Draw 12 ctx.fillStyle = 'blue'; 13 ctx.fillRect(10, 10, 100, 100); 14 }, []); 15 16 return <canvas ref={canvasRef} width={500} height={500} />; 17}

Best Practices#

Usage: ✓ Use refs for DOM access ✓ Store mutable values that don't trigger renders ✓ Use forwardRef for reusable components ✓ Prefer controlled components when possible Patterns: ✓ useImperativeHandle for custom API ✓ Callback refs for measurement ✓ Merge refs when needed ✓ Keep refs stable Avoid: ✗ Overusing refs for state ✗ Directly mutating DOM in render ✗ Ignoring null checks ✗ Creating refs in loops without keys

Conclusion#

Refs enable direct DOM access and mutable value storage. Use forwardRef for component composition, useImperativeHandle for custom APIs, and callback refs for dynamic scenarios. Always handle null cases and prefer React's declarative patterns when possible.

Share this article

Help spread the word about Bootspring