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.