forwardRef — is a React API that allows parent components to pass refs through to their children. It’s needed when you want to access DOM elements or component instances of child components from parent components.
Basic usage:
// Without forwardRef - ref won't work
const Button = (props) => <button>{props.children}</button>;
// With forwardRef - ref passes through
const Button = React.forwardRef((props, ref) => (
<button ref={ref}>{props.children}</button>
));
React forwardRef solves a specific problem: by default, function components cannot receive refs as props. ForwardRef creates a special component that can accept a ref and forward it to a DOM element or another component.
// Parent component tries to pass ref
function Parent() {
const buttonRef = useRef(null);
return (
// ❌ This won't work - ref will be undefined
<FancyButton ref={buttonRef}>Click me</FancyButton>
);
}
// Child component can't receive ref
function FancyButton(props) {
// props.ref is undefined!
return <button className="fancy">{props.children}</button>;
}
// Child component with forwardRef
const FancyButton = React.forwardRef((props, ref) => {
return (
<button ref={ref} className="fancy">
{props.children}
</button>
);
});
// Parent component
function Parent() {
const buttonRef = useRef(null);
const handleClick = () => {
// ✅ Now we can access the button
buttonRef.current.focus();
};
return (
<>
<FancyButton ref={buttonRef}>Click me</FancyButton>
<button onClick={handleClick}>Focus fancy button</button>
</>
);
}
// Custom input component
const CustomInput = React.forwardRef((props, ref) => {
return (
<div className="input-wrapper">
<label>{props.label}</label>
<input ref={ref} {...props} />
</div>
);
});
// Usage in form
function LoginForm() {
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log(usernameRef.current.value);
console.log(passwordRef.current.value);
};
useEffect(() => {
// Auto-focus on mount
usernameRef.current.focus();
}, []);
return (
<form onSubmit={handleSubmit}>
<CustomInput ref={usernameRef} label="Username" />
<CustomInput ref={passwordRef} label="Password" type="password" />
<button type="submit">Login</button>
</form>
);
}
// Wrapper for third-party component
const MapWrapper = React.forwardRef((props, ref) => {
const mapContainerRef = useRef(null);
useImperativeHandle(ref, () => ({
// Expose custom methods
centerMap: (lat, lng) => {
if (mapContainerRef.current) {
// Call third-party library method
mapLibrary.setCenter(mapContainerRef.current, { lat, lng });
}
},
getZoom: () => {
return mapLibrary.getZoom(mapContainerRef.current);
}
}));
useEffect(() => {
// Initialize third-party library
mapLibrary.init(mapContainerRef.current, props.options);
}, []);
return <div ref={mapContainerRef} className="map-container" />;
});
// Parent component
function App() {
const mapRef = useRef(null);
const handleCenterMap = () => {
mapRef.current.centerMap(40.7128, -74.0060); // New York
};
return (
<div>
<MapWrapper ref={mapRef} options={{ zoom: 10 }} />
<button onClick={handleCenterMap}>Center on New York</button>
</div>
);
}
// Reusable modal component
const Modal = React.forwardRef((props, ref) => {
const [isOpen, setIsOpen] = useState(false);
const modalRef = useRef(null);
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(prev => !prev)
}));
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div
ref={modalRef}
className="modal-content"
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<button
className="modal-close"
onClick={() => setIsOpen(false)}
>
×
</button>
{props.children}
</div>
</div>
);
});
// Usage
function App() {
const modalRef = useRef(null);
return (
<div>
<button onClick={() => modalRef.current.open()}>
Open Modal
</button>
<Modal ref={modalRef}>
<h2>Modal Content</h2>
<p>This is a reusable modal component</p>
</Modal>
</div>
);
}
// Component that needs both internal and forwarded ref
const VideoPlayer = React.forwardRef((props, forwardedRef) => {
const internalRef = useRef(null);
// Combine refs
const setRefs = useCallback((node) => {
// Set internal ref
internalRef.current = node;
// Set forwarded ref
if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(node);
} else {
forwardedRef.current = node;
}
}
}, [forwardedRef]);
useEffect(() => {
// Use internal ref for component logic
if (internalRef.current) {
internalRef.current.volume = 0.5;
}
}, []);
return <video ref={setRefs} {...props} />;
});
// Forward ref to different elements based on props
const FlexibleInput = React.forwardRef((props, ref) => {
if (props.multiline) {
return <textarea ref={ref} {...props} />;
}
if (props.type === 'select') {
return (
<select ref={ref} {...props}>
{props.options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
return <input ref={ref} {...props} />;
});
// Higher-order component that preserves ref
function withTooltip(Component) {
const WithTooltipComponent = React.forwardRef((props, ref) => {
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="tooltip-wrapper">
<Component
{...props}
ref={ref}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
/>
{showTooltip && (
<div className="tooltip">{props.tooltip}</div>
)}
</div>
);
});
// Set display name for debugging
WithTooltipComponent.displayName =
`withTooltip(${Component.displayName || Component.name})`;
return WithTooltipComponent;
}
// Usage
const ButtonWithTooltip = withTooltip(
React.forwardRef((props, ref) => (
<button ref={ref} {...props} />
))
);
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
children: React.ReactNode;
onClick?: () => void;
}
// Type-safe forwardRef component
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { variant = 'primary', size = 'medium', ...rest } = props;
return (
<button
ref={ref}
className={`btn btn-${variant} btn-${size}`}
{...rest}
/>
);
}
);
// Usage with TypeScript
function App() {
const buttonRef = useRef<HTMLButtonElement>(null);
const focusButton = () => {
buttonRef.current?.focus();
};
return (
<Button ref={buttonRef} variant="primary">
Click me
</Button>
);
}
// Custom ref handle interface
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seekTo: (time: number) => void;
}
interface VideoPlayerProps {
src: string;
poster?: string;
autoPlay?: boolean;
}
const VideoPlayer = React.forwardRef<VideoPlayerHandle, VideoPlayerProps>(
(props, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => {
videoRef.current?.play();
},
pause: () => {
videoRef.current?.pause();
},
seekTo: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
}
}), []);
return (
<video
ref={videoRef}
src={props.src}
poster={props.poster}
autoPlay={props.autoPlay}
/>
);
}
);
// Type-safe usage
function App() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seekTo(30)}>
Skip to 30s
</button>
</div>
);
}
// Optimized component with memo and forwardRef
const ExpensiveComponent = React.memo(
React.forwardRef((props, ref) => {
console.log('ExpensiveComponent rendered');
// Heavy computations
const result = useMemo(() => {
return heavyCalculation(props.data);
}, [props.data]);
return (
<div ref={ref}>
{result}
</div>
);
})
);
// Custom comparison function
const OptimizedComponent = React.memo(
React.forwardRef((props, ref) => {
return <div ref={ref}>{props.content}</div>;
}),
(prevProps, nextProps) => {
// Custom comparison logic
return prevProps.content === nextProps.content;
}
);
// ❌ Bad - no display name
const MyComponent = React.forwardRef((props, ref) => {
return <div ref={ref} />;
});
// ✅ Good - with display name
const MyComponent = React.forwardRef((props, ref) => {
return <div ref={ref} />;
});
MyComponent.displayName = 'MyComponent';
// ❌ Wrong - ref points to React component, not DOM
const Card = React.forwardRef((props, ref) => {
return <AnotherComponent ref={ref} />;
});
// ✅ Correct - ref points to DOM element
const Card = React.forwardRef((props, ref) => {
return (
<div ref={ref} className="card">
<AnotherComponent />
</div>
);
});
const FlexibleComponent = React.forwardRef((props, ref) => {
const setRef = (node) => {
// Handle both ref types
if (ref) {
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
}
};
return <div ref={setRef}>{props.children}</div>;
});
ForwardRef is an essential tool for building reusable React components that need to expose their internal DOM elements or imperative APIs to parent components.
Want more interview preparation articles? Subscribe to EasyAdvice, bookmark the site and improve every day 💪