Back to Blog
ReactRefsDOMComponents

React Refs and Forwarding

Master refs in React. From useRef to forwardRef to imperative handles and DOM access.

B
Bootspring Team
Engineering
September 26, 2020
7 min read

Refs provide direct access to DOM elements and component instances. Here's how to use them effectively.

Basic useRef#

1import { useRef, useEffect } from 'react'; 2 3function TextInput() { 4 const inputRef = useRef<HTMLInputElement>(null); 5 6 useEffect(() => { 7 // Focus on mount 8 inputRef.current?.focus(); 9 }, []); 10 11 const handleClick = () => { 12 // Access DOM directly 13 inputRef.current?.select(); 14 }; 15 16 return ( 17 <div> 18 <input ref={inputRef} type="text" /> 19 <button onClick={handleClick}>Select All</button> 20 </div> 21 ); 22} 23 24// Store mutable values without re-renders 25function Timer() { 26 const intervalRef = useRef<NodeJS.Timeout | null>(null); 27 const countRef = useRef(0); 28 29 const startTimer = () => { 30 intervalRef.current = setInterval(() => { 31 countRef.current += 1; 32 console.log(countRef.current); 33 }, 1000); 34 }; 35 36 const stopTimer = () => { 37 if (intervalRef.current) { 38 clearInterval(intervalRef.current); 39 } 40 }; 41 42 return ( 43 <div> 44 <button onClick={startTimer}>Start</button> 45 <button onClick={stopTimer}>Stop</button> 46 </div> 47 ); 48}

Callback Refs#

1import { useCallback, useState } from 'react'; 2 3function MeasuredComponent() { 4 const [height, setHeight] = useState(0); 5 6 // Callback ref - called when element mounts/unmounts 7 const measuredRef = useCallback((node: HTMLDivElement | null) => { 8 if (node !== null) { 9 setHeight(node.getBoundingClientRect().height); 10 } 11 }, []); 12 13 return ( 14 <div> 15 <div ref={measuredRef}> 16 Content that we want to measure 17 </div> 18 <p>Height: {height}px</p> 19 </div> 20 ); 21} 22 23// Handling multiple refs 24function MultipleRefs() { 25 const refsMap = useRef(new Map<string, HTMLDivElement>()); 26 27 const setRef = (id: string) => (node: HTMLDivElement | null) => { 28 if (node) { 29 refsMap.current.set(id, node); 30 } else { 31 refsMap.current.delete(id); 32 } 33 }; 34 35 const scrollToItem = (id: string) => { 36 const node = refsMap.current.get(id); 37 node?.scrollIntoView({ behavior: 'smooth' }); 38 }; 39 40 return ( 41 <div> 42 {['a', 'b', 'c'].map((id) => ( 43 <div key={id} ref={setRef(id)}> 44 Item {id} 45 </div> 46 ))} 47 <button onClick={() => scrollToItem('c')}>Go to C</button> 48 </div> 49 ); 50}

forwardRef#

1import { forwardRef, useRef } from 'react'; 2 3// Forward ref to child component 4interface InputProps { 5 label: string; 6 placeholder?: string; 7} 8 9const FancyInput = forwardRef<HTMLInputElement, InputProps>( 10 ({ label, placeholder }, ref) => { 11 return ( 12 <div className="fancy-input"> 13 <label>{label}</label> 14 <input ref={ref} placeholder={placeholder} /> 15 </div> 16 ); 17 } 18); 19 20FancyInput.displayName = 'FancyInput'; 21 22// Usage 23function Form() { 24 const inputRef = useRef<HTMLInputElement>(null); 25 26 const focusInput = () => { 27 inputRef.current?.focus(); 28 }; 29 30 return ( 31 <div> 32 <FancyInput 33 ref={inputRef} 34 label="Username" 35 placeholder="Enter username" 36 /> 37 <button onClick={focusInput}>Focus Input</button> 38 </div> 39 ); 40}

useImperativeHandle#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3// Define handle type 4interface ModalHandle { 5 open: () => void; 6 close: () => void; 7 toggle: () => void; 8} 9 10interface ModalProps { 11 title: string; 12 children: React.ReactNode; 13} 14 15const Modal = forwardRef<ModalHandle, ModalProps>( 16 ({ title, children }, ref) => { 17 const [isOpen, setIsOpen] = useState(false); 18 const dialogRef = useRef<HTMLDialogElement>(null); 19 20 // Expose custom methods via ref 21 useImperativeHandle(ref, () => ({ 22 open: () => { 23 setIsOpen(true); 24 dialogRef.current?.showModal(); 25 }, 26 close: () => { 27 setIsOpen(false); 28 dialogRef.current?.close(); 29 }, 30 toggle: () => { 31 if (isOpen) { 32 dialogRef.current?.close(); 33 } else { 34 dialogRef.current?.showModal(); 35 } 36 setIsOpen(!isOpen); 37 }, 38 }), [isOpen]); 39 40 return ( 41 <dialog ref={dialogRef}> 42 <h2>{title}</h2> 43 {children} 44 <button onClick={() => dialogRef.current?.close()}> 45 Close 46 </button> 47 </dialog> 48 ); 49 } 50); 51 52// Usage 53function App() { 54 const modalRef = useRef<ModalHandle>(null); 55 56 return ( 57 <div> 58 <button onClick={() => modalRef.current?.open()}> 59 Open Modal 60 </button> 61 <Modal ref={modalRef} title="My Modal"> 62 <p>Modal content here</p> 63 </Modal> 64 </div> 65 ); 66}

Video Player Example#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3interface VideoPlayerHandle { 4 play: () => void; 5 pause: () => void; 6 seek: (time: number) => void; 7 getCurrentTime: () => number; 8 getDuration: () => number; 9} 10 11interface VideoPlayerProps { 12 src: string; 13 poster?: string; 14} 15 16const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>( 17 ({ src, poster }, ref) => { 18 const videoRef = useRef<HTMLVideoElement>(null); 19 20 useImperativeHandle(ref, () => ({ 21 play: () => { 22 videoRef.current?.play(); 23 }, 24 pause: () => { 25 videoRef.current?.pause(); 26 }, 27 seek: (time: number) => { 28 if (videoRef.current) { 29 videoRef.current.currentTime = time; 30 } 31 }, 32 getCurrentTime: () => { 33 return videoRef.current?.currentTime ?? 0; 34 }, 35 getDuration: () => { 36 return videoRef.current?.duration ?? 0; 37 }, 38 }), []); 39 40 return ( 41 <video 42 ref={videoRef} 43 src={src} 44 poster={poster} 45 controls 46 /> 47 ); 48 } 49); 50 51// Usage 52function VideoPage() { 53 const playerRef = useRef<VideoPlayerHandle>(null); 54 55 const skipForward = () => { 56 const current = playerRef.current?.getCurrentTime() ?? 0; 57 playerRef.current?.seek(current + 10); 58 }; 59 60 return ( 61 <div> 62 <VideoPlayer 63 ref={playerRef} 64 src="/video.mp4" 65 /> 66 <button onClick={() => playerRef.current?.play()}>Play</button> 67 <button onClick={() => playerRef.current?.pause()}>Pause</button> 68 <button onClick={skipForward}>+10s</button> 69 </div> 70 ); 71}

Form Control Example#

1import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; 2 3interface FormHandle { 4 submit: () => void; 5 reset: () => void; 6 validate: () => boolean; 7 getValues: () => Record<string, string>; 8} 9 10interface FormProps { 11 onSubmit: (data: Record<string, string>) => void; 12 children: React.ReactNode; 13} 14 15const Form = forwardRef<FormHandle, FormProps>( 16 ({ onSubmit, children }, ref) => { 17 const formRef = useRef<HTMLFormElement>(null); 18 const [errors, setErrors] = useState<Record<string, string>>({}); 19 20 useImperativeHandle(ref, () => ({ 21 submit: () => { 22 formRef.current?.requestSubmit(); 23 }, 24 reset: () => { 25 formRef.current?.reset(); 26 setErrors({}); 27 }, 28 validate: () => { 29 if (!formRef.current) return false; 30 return formRef.current.checkValidity(); 31 }, 32 getValues: () => { 33 if (!formRef.current) return {}; 34 const formData = new FormData(formRef.current); 35 return Object.fromEntries(formData.entries()) as Record<string, string>; 36 }, 37 }), []); 38 39 const handleSubmit = (e: React.FormEvent) => { 40 e.preventDefault(); 41 if (!formRef.current) return; 42 43 const formData = new FormData(formRef.current); 44 const data = Object.fromEntries(formData.entries()) as Record<string, string>; 45 onSubmit(data); 46 }; 47 48 return ( 49 <form ref={formRef} onSubmit={handleSubmit}> 50 {children} 51 </form> 52 ); 53 } 54); 55 56// Usage 57function App() { 58 const formRef = useRef<FormHandle>(null); 59 60 const handleSave = () => { 61 if (formRef.current?.validate()) { 62 formRef.current.submit(); 63 } else { 64 alert('Please fix validation errors'); 65 } 66 }; 67 68 return ( 69 <div> 70 <Form ref={formRef} onSubmit={(data) => console.log(data)}> 71 <input name="email" type="email" required /> 72 <input name="password" type="password" required /> 73 </Form> 74 <button onClick={handleSave}>Save</button> 75 <button onClick={() => formRef.current?.reset()}>Reset</button> 76 </div> 77 ); 78}

Merging Refs#

1import { useRef, useCallback, RefCallback, MutableRefObject } from 'react'; 2 3// Utility to merge multiple refs 4function mergeRefs<T>( 5 ...refs: (RefCallback<T> | MutableRefObject<T> | null)[] 6): RefCallback<T> { 7 return (instance: T) => { 8 refs.forEach((ref) => { 9 if (typeof ref === 'function') { 10 ref(instance); 11 } else if (ref != null) { 12 ref.current = instance; 13 } 14 }); 15 }; 16} 17 18// Usage with forwardRef 19interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {} 20 21const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { 22 const internalRef = useRef<HTMLButtonElement>(null); 23 24 // Use both refs 25 const mergedRef = mergeRefs(ref, internalRef); 26 27 return <button ref={mergedRef} {...props} />; 28}); 29 30// Hook version 31function useMergedRef<T>(...refs: (RefCallback<T> | MutableRefObject<T> | null)[]) { 32 return useCallback( 33 (instance: T) => { 34 refs.forEach((ref) => { 35 if (typeof ref === 'function') { 36 ref(instance); 37 } else if (ref != null) { 38 ref.current = instance; 39 } 40 }); 41 }, 42 // eslint-disable-next-line react-hooks/exhaustive-deps 43 refs 44 ); 45}

Avoiding Common Pitfalls#

1// DON'T: Access ref in render 2function Bad() { 3 const ref = useRef<HTMLInputElement>(null); 4 5 // ref.current is null during render! 6 console.log(ref.current?.value); // undefined 7 8 return <input ref={ref} />; 9} 10 11// DO: Access ref in effects or handlers 12function Good() { 13 const ref = useRef<HTMLInputElement>(null); 14 15 useEffect(() => { 16 console.log(ref.current?.value); // Works 17 }, []); 18 19 const handleClick = () => { 20 console.log(ref.current?.value); // Works 21 }; 22 23 return <input ref={ref} />; 24} 25 26// DON'T: Use refs for reactive data 27function BadReactive() { 28 const countRef = useRef(0); 29 30 // This won't update the UI! 31 const increment = () => { 32 countRef.current += 1; 33 }; 34 35 return <p>{countRef.current}</p>; 36} 37 38// DO: Use state for reactive data 39function GoodReactive() { 40 const [count, setCount] = useState(0); 41 42 const increment = () => { 43 setCount(c => c + 1); 44 }; 45 46 return <p>{count}</p>; 47}

Best Practices#

When to Use Refs: ✓ DOM element access (focus, scroll, measure) ✓ Storing timeout/interval IDs ✓ Storing previous values ✓ Imperative animations ✓ Third-party library integration When NOT to Use Refs: ✗ Data that affects rendering (use state) ✗ Avoiding re-renders (consider memo) ✗ Storing component state ✗ Communication between components forwardRef: ✓ Use for reusable components ✓ Add displayName for debugging ✓ Document exposed ref API ✓ Consider useImperativeHandle useImperativeHandle: ✓ Expose minimal API ✓ Hide implementation details ✓ Type the handle interface ✓ Include cleanup in return

Conclusion#

Refs provide escape hatches for DOM access and imperative operations. Use useRef for direct element access, forwardRef to pass refs through components, and useImperativeHandle to expose custom APIs. Remember that refs don't trigger re-renders and should be accessed in effects or event handlers.

Share this article

Help spread the word about Bootspring