Back to Blog
ReactforwardRefRefsComponents

React forwardRef Guide

Master React forwardRef for passing refs through components and building reusable component libraries.

B
Bootspring Team
Engineering
March 14, 2020
6 min read

forwardRef lets you pass refs through components to access child DOM elements. Here's how to use it.

Basic Usage#

1import { forwardRef, useRef } from 'react'; 2 3// Without forwardRef - ref doesn't reach input 4function BrokenInput({ label }) { 5 return ( 6 <div> 7 <label>{label}</label> 8 <input /> 9 </div> 10 ); 11} 12 13// With forwardRef - ref reaches input 14const Input = forwardRef<HTMLInputElement, { label: string }>( 15 function Input({ label }, ref) { 16 return ( 17 <div> 18 <label>{label}</label> 19 <input ref={ref} /> 20 </div> 21 ); 22 } 23); 24 25// Usage 26function Form() { 27 const inputRef = useRef<HTMLInputElement>(null); 28 29 const focusInput = () => { 30 inputRef.current?.focus(); 31 }; 32 33 return ( 34 <div> 35 <Input ref={inputRef} label="Email" /> 36 <button onClick={focusInput}>Focus</button> 37 </div> 38 ); 39}

TypeScript Patterns#

1import { forwardRef, ComponentPropsWithoutRef, ElementRef } from 'react'; 2 3// Basic typing 4interface ButtonProps { 5 variant?: 'primary' | 'secondary'; 6 children: React.ReactNode; 7} 8 9const Button = forwardRef<HTMLButtonElement, ButtonProps>( 10 function Button({ variant = 'primary', children }, ref) { 11 return ( 12 <button ref={ref} className={`btn-${variant}`}> 13 {children} 14 </button> 15 ); 16 } 17); 18 19// Extend native element props 20type InputProps = ComponentPropsWithoutRef<'input'> & { 21 label?: string; 22 error?: string; 23}; 24 25const TextInput = forwardRef<HTMLInputElement, InputProps>( 26 function TextInput({ label, error, className, ...props }, ref) { 27 return ( 28 <div className="input-wrapper"> 29 {label && <label>{label}</label>} 30 <input 31 ref={ref} 32 className={`input ${error ? 'error' : ''} ${className || ''}`} 33 {...props} 34 /> 35 {error && <span className="error-text">{error}</span>} 36 </div> 37 ); 38 } 39); 40 41// ElementRef helper 42type DivRef = ElementRef<'div'>; 43type ButtonRef = ElementRef<typeof Button>;

Custom Component Library#

1import { forwardRef, ComponentPropsWithoutRef } from 'react'; 2 3// Card component 4interface CardProps extends ComponentPropsWithoutRef<'div'> { 5 variant?: 'elevated' | 'outlined'; 6} 7 8const Card = forwardRef<HTMLDivElement, CardProps>( 9 function Card({ variant = 'elevated', className, children, ...props }, ref) { 10 return ( 11 <div 12 ref={ref} 13 className={`card card-${variant} ${className || ''}`} 14 {...props} 15 > 16 {children} 17 </div> 18 ); 19 } 20); 21 22// Card.Header, Card.Body, Card.Footer 23const CardHeader = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>( 24 function CardHeader({ className, children, ...props }, ref) { 25 return ( 26 <div ref={ref} className={`card-header ${className || ''}`} {...props}> 27 {children} 28 </div> 29 ); 30 } 31); 32 33const CardBody = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>( 34 function CardBody({ className, children, ...props }, ref) { 35 return ( 36 <div ref={ref} className={`card-body ${className || ''}`} {...props}> 37 {children} 38 </div> 39 ); 40 } 41); 42 43// Attach sub-components 44const CardNamespace = Object.assign(Card, { 45 Header: CardHeader, 46 Body: CardBody, 47}); 48 49export { CardNamespace as Card }; 50 51// Usage 52function App() { 53 const cardRef = useRef<HTMLDivElement>(null); 54 55 return ( 56 <Card ref={cardRef} variant="outlined"> 57 <Card.Header>Title</Card.Header> 58 <Card.Body>Content</Card.Body> 59 </Card> 60 ); 61}

useImperativeHandle#

1import { forwardRef, useRef, useImperativeHandle } from 'react'; 2 3// Custom ref API 4interface InputHandle { 5 focus: () => void; 6 clear: () => void; 7 getValue: () => string; 8} 9 10interface CustomInputProps { 11 label: string; 12 defaultValue?: string; 13} 14 15const CustomInput = forwardRef<InputHandle, CustomInputProps>( 16 function CustomInput({ label, defaultValue }, ref) { 17 const inputRef = useRef<HTMLInputElement>(null); 18 19 useImperativeHandle(ref, () => ({ 20 focus() { 21 inputRef.current?.focus(); 22 }, 23 clear() { 24 if (inputRef.current) { 25 inputRef.current.value = ''; 26 } 27 }, 28 getValue() { 29 return inputRef.current?.value || ''; 30 }, 31 }), []); 32 33 return ( 34 <div> 35 <label>{label}</label> 36 <input ref={inputRef} defaultValue={defaultValue} /> 37 </div> 38 ); 39 } 40); 41 42// Usage 43function Form() { 44 const inputRef = useRef<InputHandle>(null); 45 46 const handleSubmit = () => { 47 const value = inputRef.current?.getValue(); 48 console.log('Value:', value); 49 inputRef.current?.clear(); 50 }; 51 52 return ( 53 <div> 54 <CustomInput ref={inputRef} label="Name" /> 55 <button onClick={() => inputRef.current?.focus()}>Focus</button> 56 <button onClick={handleSubmit}>Submit</button> 57 </div> 58 ); 59}
1import { forwardRef, useRef, useImperativeHandle, useState } from 'react'; 2 3interface ModalHandle { 4 open: () => void; 5 close: () => void; 6 isOpen: () => boolean; 7} 8 9interface ModalProps { 10 title: string; 11 children: React.ReactNode; 12 onClose?: () => void; 13} 14 15const Modal = forwardRef<ModalHandle, ModalProps>( 16 function Modal({ title, children, onClose }, ref) { 17 const [isOpen, setIsOpen] = useState(false); 18 19 useImperativeHandle(ref, () => ({ 20 open() { 21 setIsOpen(true); 22 }, 23 close() { 24 setIsOpen(false); 25 onClose?.(); 26 }, 27 isOpen() { 28 return isOpen; 29 }, 30 }), [isOpen, onClose]); 31 32 if (!isOpen) return null; 33 34 return ( 35 <div className="modal-overlay"> 36 <div className="modal"> 37 <header> 38 <h2>{title}</h2> 39 <button onClick={() => setIsOpen(false)}>×</button> 40 </header> 41 <div className="modal-content"> 42 {children} 43 </div> 44 </div> 45 </div> 46 ); 47 } 48); 49 50// Usage 51function App() { 52 const modalRef = useRef<ModalHandle>(null); 53 54 return ( 55 <div> 56 <button onClick={() => modalRef.current?.open()}> 57 Open Modal 58 </button> 59 <Modal ref={modalRef} title="Welcome"> 60 <p>Modal content here</p> 61 <button onClick={() => modalRef.current?.close()}> 62 Close 63 </button> 64 </Modal> 65 </div> 66 ); 67}

Polymorphic Components#

1import { forwardRef, ComponentPropsWithoutRef, ElementType } from 'react'; 2 3type PolymorphicRef<T extends ElementType> = 4 ComponentPropsWithoutRef<T>['ref']; 5 6type PolymorphicProps<T extends ElementType, Props = {}> = Props & 7 Omit<ComponentPropsWithoutRef<T>, keyof Props | 'as'> & { 8 as?: T; 9 }; 10 11// Button that can render as different elements 12type ButtonProps<T extends ElementType = 'button'> = PolymorphicProps<T, { 13 variant?: 'primary' | 'secondary'; 14}>; 15 16function ButtonInner<T extends ElementType = 'button'>( 17 { as, variant = 'primary', className, ...props }: ButtonProps<T>, 18 ref: PolymorphicRef<T> 19) { 20 const Component = as || 'button'; 21 22 return ( 23 <Component 24 ref={ref} 25 className={`btn btn-${variant} ${className || ''}`} 26 {...props} 27 /> 28 ); 29} 30 31const Button = forwardRef(ButtonInner) as <T extends ElementType = 'button'>( 32 props: ButtonProps<T> & { ref?: PolymorphicRef<T> } 33) => React.ReactElement | null; 34 35// Usage 36function App() { 37 const buttonRef = useRef<HTMLButtonElement>(null); 38 const linkRef = useRef<HTMLAnchorElement>(null); 39 40 return ( 41 <> 42 <Button ref={buttonRef} variant="primary"> 43 Click Me 44 </Button> 45 46 <Button as="a" ref={linkRef} href="/about" variant="secondary"> 47 Link Button 48 </Button> 49 </> 50 ); 51}

HOC with forwardRef#

1import { forwardRef, ComponentType, ComponentPropsWithoutRef } from 'react'; 2 3// Wrap component while preserving ref 4function withLogging<T extends ComponentType<any>>( 5 WrappedComponent: T 6) { 7 type Props = ComponentPropsWithoutRef<T>; 8 type Ref = T extends ComponentType<infer P> 9 ? P extends { ref?: infer R } ? R : never 10 : never; 11 12 const WithLogging = forwardRef<Ref, Props>( 13 function WithLogging(props, ref) { 14 console.log('Rendering:', WrappedComponent.name); 15 16 return <WrappedComponent {...props} ref={ref} />; 17 } 18 ); 19 20 WithLogging.displayName = `WithLogging(${ 21 WrappedComponent.displayName || WrappedComponent.name 22 })`; 23 24 return WithLogging; 25} 26 27// Usage 28const LoggedInput = withLogging( 29 forwardRef<HTMLInputElement, { placeholder: string }>( 30 function Input({ placeholder }, ref) { 31 return <input ref={ref} placeholder={placeholder} />; 32 } 33 ) 34);

Common Patterns#

1// Merge refs 2function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) { 3 return (value: T) => { 4 refs.forEach(ref => { 5 if (typeof ref === 'function') { 6 ref(value); 7 } else if (ref) { 8 (ref as React.MutableRefObject<T>).current = value; 9 } 10 }); 11 }; 12} 13 14const Input = forwardRef<HTMLInputElement, InputProps>( 15 function Input(props, forwardedRef) { 16 const internalRef = useRef<HTMLInputElement>(null); 17 18 return ( 19 <input 20 ref={mergeRefs(forwardedRef, internalRef)} 21 {...props} 22 /> 23 ); 24 } 25); 26 27// Ref callback 28const CallbackInput = forwardRef<HTMLInputElement, {}>( 29 function CallbackInput(props, ref) { 30 const handleRef = (element: HTMLInputElement | null) => { 31 // Do something with element 32 console.log('Element:', element); 33 34 // Forward ref 35 if (typeof ref === 'function') { 36 ref(element); 37 } else if (ref) { 38 ref.current = element; 39 } 40 }; 41 42 return <input ref={handleRef} {...props} />; 43 } 44);

Best Practices#

Usage: ✓ Use for reusable component libraries ✓ Expose DOM refs when needed ✓ Use useImperativeHandle sparingly ✓ Add displayName for debugging TypeScript: ✓ Type refs properly ✓ Extend native element props ✓ Use ComponentPropsWithoutRef ✓ Document custom ref APIs Patterns: ✓ Forward refs in HOCs ✓ Merge internal and forwarded refs ✓ Keep ref APIs minimal ✓ Prefer props over imperative Avoid: ✗ Overusing imperative patterns ✗ Exposing too much via ref ✗ Forgetting to forward refs ✗ Complex ref dependencies

Conclusion#

forwardRef enables passing refs through components to access underlying DOM elements. Use it for building component libraries, custom inputs, and any reusable component that wraps native elements. Combine with useImperativeHandle when you need custom imperative APIs, but prefer declarative patterns when possible.

Share this article

Help spread the word about Bootspring