Back to Blog
ReactforwardRefRefsComponents

React forwardRef Patterns Guide

Master React forwardRef for passing refs through components to access DOM elements and child methods.

B
Bootspring Team
Engineering
June 20, 2019
6 min read

forwardRef allows components to receive a ref and forward it to a child element. Here's how to use it effectively.

Basic Usage#

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

With Additional Props#

1import { forwardRef } from 'react'; 2 3interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { 4 variant?: 'primary' | 'secondary'; 5 size?: 'sm' | 'md' | 'lg'; 6 isLoading?: boolean; 7} 8 9const Button = forwardRef<HTMLButtonElement, ButtonProps>( 10 ({ variant = 'primary', size = 'md', isLoading, children, ...props }, ref) => { 11 const className = `btn btn-${variant} btn-${size}`; 12 13 return ( 14 <button ref={ref} className={className} disabled={isLoading} {...props}> 15 {isLoading ? 'Loading...' : children} 16 </button> 17 ); 18 } 19); 20 21Button.displayName = 'Button';

Composing Components#

1import { forwardRef } from 'react'; 2 3// Base input component 4const BaseInput = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( 5 (props, ref) => { 6 return <input ref={ref} className="base-input" {...props} />; 7 } 8); 9 10// Extended input with label 11interface LabeledInputProps extends React.InputHTMLAttributes<HTMLInputElement> { 12 label: string; 13 error?: string; 14} 15 16const LabeledInput = forwardRef<HTMLInputElement, LabeledInputProps>( 17 ({ label, error, id, ...props }, ref) => { 18 const inputId = id || label.toLowerCase().replace(/\s+/g, '-'); 19 20 return ( 21 <div className="form-field"> 22 <label htmlFor={inputId}>{label}</label> 23 <BaseInput ref={ref} id={inputId} {...props} /> 24 {error && <span className="error">{error}</span>} 25 </div> 26 ); 27 } 28);

With useImperativeHandle#

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

Multiple Refs#

1import { forwardRef, useRef } from 'react'; 2 3interface FormHandle { 4 focus: () => void; 5 reset: () => void; 6 submit: () => void; 7} 8 9interface FormProps { 10 onSubmit: (data: FormData) => void; 11} 12 13const Form = forwardRef<FormHandle, FormProps>(({ onSubmit }, ref) => { 14 const formRef = useRef<HTMLFormElement>(null); 15 const inputRef = useRef<HTMLInputElement>(null); 16 17 useImperativeHandle(ref, () => ({ 18 focus: () => inputRef.current?.focus(), 19 reset: () => formRef.current?.reset(), 20 submit: () => formRef.current?.requestSubmit(), 21 })); 22 23 const handleSubmit = (e: React.FormEvent) => { 24 e.preventDefault(); 25 if (formRef.current) { 26 onSubmit(new FormData(formRef.current)); 27 } 28 }; 29 30 return ( 31 <form ref={formRef} onSubmit={handleSubmit}> 32 <input ref={inputRef} name="email" type="email" /> 33 <button type="submit">Submit</button> 34 </form> 35 ); 36});

Generic forwardRef#

1import { forwardRef, Ref } from 'react'; 2 3// Generic list component 4interface ListProps<T> { 5 items: T[]; 6 renderItem: (item: T, index: number) => React.ReactNode; 7 className?: string; 8} 9 10function ListInner<T>( 11 { items, renderItem, className }: ListProps<T>, 12 ref: Ref<HTMLUListElement> 13) { 14 return ( 15 <ul ref={ref} className={className}> 16 {items.map((item, index) => ( 17 <li key={index}>{renderItem(item, index)}</li> 18 ))} 19 </ul> 20 ); 21} 22 23// Type assertion needed for generic forwardRef 24const List = forwardRef(ListInner) as <T>( 25 props: ListProps<T> & { ref?: Ref<HTMLUListElement> } 26) => React.ReactElement; 27 28// Usage 29function App() { 30 const listRef = useRef<HTMLUListElement>(null); 31 const items = [{ name: 'Item 1' }, { name: 'Item 2' }]; 32 33 return ( 34 <List 35 ref={listRef} 36 items={items} 37 renderItem={(item) => <span>{item.name}</span>} 38 /> 39 ); 40}

With Higher-Order Components#

1import { forwardRef, ComponentType, Ref } from 'react'; 2 3// HOC that adds logging 4function withLogging<P extends object>( 5 WrappedComponent: ComponentType<P> 6) { 7 const WithLogging = forwardRef<unknown, P>((props, ref) => { 8 console.log('Props:', props); 9 return <WrappedComponent {...props} ref={ref} />; 10 }); 11 12 WithLogging.displayName = `WithLogging(${ 13 WrappedComponent.displayName || WrappedComponent.name 14 })`; 15 16 return WithLogging; 17} 18 19// HOC that adds styling 20function withClassName<P extends { className?: string }>( 21 WrappedComponent: ComponentType<P>, 22 additionalClassName: string 23) { 24 return forwardRef<unknown, P>((props, ref) => { 25 const className = [props.className, additionalClassName] 26 .filter(Boolean) 27 .join(' '); 28 29 return <WrappedComponent {...props} className={className} ref={ref} />; 30 }); 31}

Ref Callback Pattern#

1import { forwardRef, useCallback, useState } from 'react'; 2 3interface MeasuredBoxProps { 4 children: React.ReactNode; 5} 6 7const MeasuredBox = forwardRef<HTMLDivElement, MeasuredBoxProps>( 8 ({ children }, ref) => { 9 const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 10 11 // Combine external ref with measurement 12 const measureRef = useCallback( 13 (node: HTMLDivElement | null) => { 14 if (node) { 15 setDimensions({ 16 width: node.offsetWidth, 17 height: node.offsetHeight, 18 }); 19 } 20 21 // Forward to external ref 22 if (typeof ref === 'function') { 23 ref(node); 24 } else if (ref) { 25 ref.current = node; 26 } 27 }, 28 [ref] 29 ); 30 31 return ( 32 <div ref={measureRef}> 33 <div> 34 Size: {dimensions.width} x {dimensions.height} 35 </div> 36 {children} 37 </div> 38 ); 39 } 40);

Input Group Component#

1import { forwardRef, createContext, useContext, useId } from 'react'; 2 3interface InputGroupContextValue { 4 inputId: string; 5 hasError: boolean; 6} 7 8const InputGroupContext = createContext<InputGroupContextValue | null>(null); 9 10interface InputGroupProps { 11 children: React.ReactNode; 12 error?: string; 13} 14 15function InputGroup({ children, error }: InputGroupProps) { 16 const inputId = useId(); 17 18 return ( 19 <InputGroupContext.Provider value={{ inputId, hasError: !!error }}> 20 <div className="input-group"> 21 {children} 22 {error && <span className="error">{error}</span>} 23 </div> 24 </InputGroupContext.Provider> 25 ); 26} 27 28const InputGroupLabel = ({ children }: { children: React.ReactNode }) => { 29 const context = useContext(InputGroupContext); 30 return <label htmlFor={context?.inputId}>{children}</label>; 31}; 32 33const InputGroupInput = forwardRef< 34 HTMLInputElement, 35 React.InputHTMLAttributes<HTMLInputElement> 36>((props, ref) => { 37 const context = useContext(InputGroupContext); 38 39 return ( 40 <input 41 ref={ref} 42 id={context?.inputId} 43 aria-invalid={context?.hasError} 44 {...props} 45 /> 46 ); 47}); 48 49// Compound component 50InputGroup.Label = InputGroupLabel; 51InputGroup.Input = InputGroupInput; 52 53// Usage 54function Form() { 55 const inputRef = useRef<HTMLInputElement>(null); 56 57 return ( 58 <InputGroup error="Required field"> 59 <InputGroup.Label>Email</InputGroup.Label> 60 <InputGroup.Input ref={inputRef} type="email" /> 61 </InputGroup> 62 ); 63}

Best Practices#

Typing: ✓ Use proper generic types ✓ Set displayName for debugging ✓ Type both ref and props ✓ Export prop types Patterns: ✓ useImperativeHandle for custom APIs ✓ Combine refs when needed ✓ Support ref callbacks ✓ Forward to correct element Component Design: ✓ Keep ref forwarding simple ✓ Document ref behavior ✓ Consider if ref is needed ✓ Use composition Avoid: ✗ Forwarding to wrong element ✗ Forgetting displayName ✗ Complex ref logic ✗ Breaking ref contract

Conclusion#

forwardRef enables passing refs through components to access DOM elements or expose imperative APIs. Use it with useImperativeHandle for custom ref interfaces, combine with useCallback for measurement patterns, and properly type both the ref and props. Always set displayName for easier debugging in React DevTools. Consider whether a ref is necessary, as declarative patterns often work better.

Share this article

Help spread the word about Bootspring