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.