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}Modal with Ref#
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.