Back to Blog
ReactRefsforwardRefDOM

React Ref Forwarding Guide

Master React ref forwarding for passing refs through components to DOM elements.

B
Bootspring Team
Engineering
December 14, 2018
6 min read

Ref forwarding passes a ref through a component to a child element. Here's how to use it effectively.

Basic Ref Forwarding#

1import { forwardRef, useRef } from 'react'; 2 3// Forward ref to input element 4const TextInput = forwardRef(function TextInput(props, ref) { 5 return ( 6 <input 7 ref={ref} 8 type="text" 9 className="text-input" 10 {...props} 11 /> 12 ); 13}); 14 15// Usage 16function Form() { 17 const inputRef = useRef(null); 18 19 const focusInput = () => { 20 inputRef.current?.focus(); 21 }; 22 23 return ( 24 <div> 25 <TextInput ref={inputRef} placeholder="Enter text" /> 26 <button onClick={focusInput}>Focus Input</button> 27 </div> 28 ); 29}

With TypeScript#

1import { forwardRef, useRef, InputHTMLAttributes } from 'react'; 2 3interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> { 4 label?: string; 5} 6 7const TextInput = forwardRef<HTMLInputElement, TextInputProps>( 8 function TextInput({ label, ...props }, ref) { 9 return ( 10 <div className="field"> 11 {label && <label>{label}</label>} 12 <input ref={ref} {...props} /> 13 </div> 14 ); 15 } 16); 17 18// Usage 19function Form() { 20 const inputRef = useRef<HTMLInputElement>(null); 21 22 return <TextInput ref={inputRef} label="Email" type="email" />; 23}

Button Component#

1import { forwardRef } from 'react'; 2 3const Button = forwardRef(function Button( 4 { children, variant = 'primary', ...props }, 5 ref 6) { 7 return ( 8 <button 9 ref={ref} 10 className={`btn btn-${variant}`} 11 {...props} 12 > 13 {children} 14 </button> 15 ); 16}); 17 18// Usage 19function App() { 20 const buttonRef = useRef(null); 21 22 useEffect(() => { 23 // Access button's DOM methods 24 console.log(buttonRef.current?.getBoundingClientRect()); 25 }, []); 26 27 return ( 28 <Button ref={buttonRef} variant="secondary"> 29 Click Me 30 </Button> 31 ); 32}

Forwarding to Multiple Elements#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3// Expose multiple refs via useImperativeHandle 4const FormGroup = forwardRef(function FormGroup(props, ref) { 5 const inputRef = useRef(null); 6 const labelRef = useRef(null); 7 8 useImperativeHandle(ref, () => ({ 9 input: inputRef.current, 10 label: labelRef.current, 11 focus: () => inputRef.current?.focus(), 12 clear: () => { 13 if (inputRef.current) inputRef.current.value = ''; 14 }, 15 })); 16 17 return ( 18 <div className="form-group"> 19 <label ref={labelRef}>{props.label}</label> 20 <input ref={inputRef} {...props} /> 21 </div> 22 ); 23}); 24 25// Usage 26function Form() { 27 const formGroupRef = useRef(null); 28 29 const handleClear = () => { 30 formGroupRef.current?.clear(); 31 formGroupRef.current?.focus(); 32 }; 33 34 return ( 35 <> 36 <FormGroup ref={formGroupRef} label="Username" /> 37 <button onClick={handleClear}>Clear & Focus</button> 38 </> 39 ); 40}

Custom Modal#

1import { forwardRef, useImperativeHandle, useState } from 'react'; 2 3const Modal = forwardRef(function Modal({ title, children }, ref) { 4 const [isOpen, setIsOpen] = useState(false); 5 6 useImperativeHandle(ref, () => ({ 7 open: () => setIsOpen(true), 8 close: () => setIsOpen(false), 9 toggle: () => setIsOpen(prev => !prev), 10 isOpen, 11 })); 12 13 if (!isOpen) return null; 14 15 return ( 16 <div className="modal-overlay" onClick={() => setIsOpen(false)}> 17 <div className="modal" onClick={e => e.stopPropagation()}> 18 <header> 19 <h2>{title}</h2> 20 <button onClick={() => setIsOpen(false)}>×</button> 21 </header> 22 <div className="modal-content">{children}</div> 23 </div> 24 </div> 25 ); 26}); 27 28// Usage 29function App() { 30 const modalRef = useRef(null); 31 32 return ( 33 <> 34 <button onClick={() => modalRef.current?.open()}> 35 Open Modal 36 </button> 37 <Modal ref={modalRef} title="Settings"> 38 <p>Modal content here</p> 39 </Modal> 40 </> 41 ); 42}

Scroll Container#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3const ScrollContainer = forwardRef(function ScrollContainer( 4 { children, ...props }, 5 ref 6) { 7 const containerRef = useRef(null); 8 9 useImperativeHandle(ref, () => ({ 10 scrollTo: (options) => containerRef.current?.scrollTo(options), 11 scrollToTop: () => containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }), 12 scrollToBottom: () => { 13 const el = containerRef.current; 14 if (el) { 15 el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); 16 } 17 }, 18 getScrollPosition: () => ({ 19 top: containerRef.current?.scrollTop ?? 0, 20 left: containerRef.current?.scrollLeft ?? 0, 21 }), 22 })); 23 24 return ( 25 <div ref={containerRef} className="scroll-container" {...props}> 26 {children} 27 </div> 28 ); 29}); 30 31// Usage 32function ChatWindow() { 33 const scrollRef = useRef(null); 34 35 const handleNewMessage = () => { 36 scrollRef.current?.scrollToBottom(); 37 }; 38 39 return ( 40 <ScrollContainer ref={scrollRef}> 41 {messages.map(msg => <Message key={msg.id} {...msg} />)} 42 </ScrollContainer> 43 ); 44}

Video Player#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3const VideoPlayer = forwardRef(function VideoPlayer({ src, ...props }, ref) { 4 const videoRef = useRef(null); 5 6 useImperativeHandle(ref, () => ({ 7 play: () => videoRef.current?.play(), 8 pause: () => videoRef.current?.pause(), 9 stop: () => { 10 const video = videoRef.current; 11 if (video) { 12 video.pause(); 13 video.currentTime = 0; 14 } 15 }, 16 seek: (time) => { 17 if (videoRef.current) { 18 videoRef.current.currentTime = time; 19 } 20 }, 21 getCurrentTime: () => videoRef.current?.currentTime ?? 0, 22 getDuration: () => videoRef.current?.duration ?? 0, 23 setVolume: (volume) => { 24 if (videoRef.current) { 25 videoRef.current.volume = Math.max(0, Math.min(1, volume)); 26 } 27 }, 28 })); 29 30 return <video ref={videoRef} src={src} {...props} />; 31}); 32 33// Usage 34function Player() { 35 const videoRef = useRef(null); 36 37 return ( 38 <div> 39 <VideoPlayer ref={videoRef} src="/video.mp4" /> 40 <div className="controls"> 41 <button onClick={() => videoRef.current?.play()}>Play</button> 42 <button onClick={() => videoRef.current?.pause()}>Pause</button> 43 <button onClick={() => videoRef.current?.stop()}>Stop</button> 44 </div> 45 </div> 46 ); 47}

Higher-Order Component#

1import { forwardRef } from 'react'; 2 3// HOC that forwards refs 4function withLogging(WrappedComponent) { 5 const WithLogging = forwardRef(function WithLogging(props, ref) { 6 useEffect(() => { 7 console.log(`${WrappedComponent.name} mounted`); 8 return () => console.log(`${WrappedComponent.name} unmounted`); 9 }, []); 10 11 return <WrappedComponent ref={ref} {...props} />; 12 }); 13 14 WithLogging.displayName = `WithLogging(${WrappedComponent.name})`; 15 return WithLogging; 16} 17 18// Base component with forwardRef 19const Input = forwardRef(function Input(props, ref) { 20 return <input ref={ref} {...props} />; 21}); 22 23// Enhanced component 24const LoggedInput = withLogging(Input); 25 26// Usage 27function Form() { 28 const inputRef = useRef(null); 29 return <LoggedInput ref={inputRef} />; 30}

Compound Components#

1import { forwardRef, createContext, useContext, useRef } from 'react'; 2 3const TabsContext = createContext(null); 4 5const Tabs = forwardRef(function Tabs({ children, defaultValue }, ref) { 6 const [value, setValue] = useState(defaultValue); 7 8 return ( 9 <TabsContext.Provider value={{ value, setValue }}> 10 <div ref={ref} className="tabs"> 11 {children} 12 </div> 13 </TabsContext.Provider> 14 ); 15}); 16 17const TabList = forwardRef(function TabList({ children }, ref) { 18 return ( 19 <div ref={ref} className="tab-list" role="tablist"> 20 {children} 21 </div> 22 ); 23}); 24 25const Tab = forwardRef(function Tab({ value, children }, ref) { 26 const { value: selected, setValue } = useContext(TabsContext); 27 28 return ( 29 <button 30 ref={ref} 31 role="tab" 32 aria-selected={selected === value} 33 onClick={() => setValue(value)} 34 > 35 {children} 36 </button> 37 ); 38}); 39 40Tabs.List = TabList; 41Tabs.Tab = Tab;

Best Practices#

When to Use: ✓ Wrapping DOM elements ✓ Building component libraries ✓ HOC that wrap components ✓ Compound components useImperativeHandle: ✓ Expose limited API ✓ Custom methods ✓ Multiple element refs ✓ Complex interactions TypeScript: ✓ Specify element type ✓ Define custom handle type ✓ Extend HTML attributes ✓ Document exposed methods Avoid: ✗ Exposing entire DOM node ✗ Overusing imperative APIs ✗ Forgetting displayName ✗ Complex ref hierarchies

Conclusion#

Ref forwarding enables passing refs through components to DOM elements. Use forwardRef for simple cases, combine with useImperativeHandle to expose custom APIs. This pattern is essential for component libraries, accessibility, and integrating with third-party libraries that need DOM access. Keep exposed APIs minimal and well-documented.

Share this article

Help spread the word about Bootspring