forwardRef — это API React, который позволяет родительским компонентам передавать ref дочерним компонентам. Он нужен, когда вы хотите получить доступ к DOM элементам или экземплярам дочерних компонентов из родительских компонентов.
Базовое использование:
// Без forwardRef - ref не будет работать
const Button = (props) => <button>{props.children}</button>;
// С forwardRef - ref передается дальше
const Button = React.forwardRef((props, ref) => (
<button ref={ref}>{props.children}</button>
));React forwardRef решает конкретную проблему: по умолчанию функциональные компоненты не могут получать ref как пропс. ForwardRef создает специальный компонент, который может принимать ref и передавать его DOM элементу или другому компоненту.
// Родительский компонент пытается передать ref
function Parent() {
const buttonRef = useRef(null);
return (
// ❌ Это не будет работать - ref будет undefined
<FancyButton ref={buttonRef}>Нажми меня</FancyButton>
);
}
// Дочерний компонент не может получить ref
function FancyButton(props) {
// props.ref не определен!
return <button className="fancy">{props.children}</button>;
}// Дочерний компонент с forwardRef
const FancyButton = React.forwardRef((props, ref) => {
return (
<button ref={ref} className="fancy">
{props.children}
</button>
);
});
// Родительский компонент
function Parent() {
const buttonRef = useRef(null);
const handleClick = () => {
// ✅ Теперь мы можем получить доступ к кнопке
buttonRef.current.focus();
};
return (
<>
<FancyButton ref={buttonRef}>Нажми меня</FancyButton>
<button onClick={handleClick}>Фокус на fancy кнопку</button>
</>
);
}// Кастомный компонент инпута
const CustomInput = React.forwardRef((props, ref) => {
return (
<div className="input-wrapper">
<label>{props.label}</label>
<input ref={ref} {...props} />
</div>
);
});
// Использование в форме
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(() => {
// Автофокус при монтировании
usernameRef.current.focus();
}, []);
return (
<form onSubmit={handleSubmit}>
<CustomInput ref={usernameRef} label="Имя пользователя" />
<CustomInput ref={passwordRef} label="Пароль" type="password" />
<button type="submit">Войти</button>
</form>
);
}// Обертка для стороннего компонента
const MapWrapper = React.forwardRef((props, ref) => {
const mapContainerRef = useRef(null);
useImperativeHandle(ref, () => ({
// Экспортируем кастомные методы
centerMap: (lat, lng) => {
if (mapContainerRef.current) {
// Вызываем метод сторонней библиотеки
mapLibrary.setCenter(mapContainerRef.current, { lat, lng });
}
},
getZoom: () => {
return mapLibrary.getZoom(mapContainerRef.current);
}
}));
useEffect(() => {
// Инициализируем стороннюю библиотеку
mapLibrary.init(mapContainerRef.current, props.options);
}, []);
return <div ref={mapContainerRef} className="map-container" />;
});
// Родительский компонент
function App() {
const mapRef = useRef(null);
const handleCenterMap = () => {
mapRef.current.centerMap(40.7128, -74.0060); // Нью-Йорк
};
return (
<div>
<MapWrapper ref={mapRef} options={{ zoom: 10 }} />
<button onClick={handleCenterMap}>Центрировать на Нью-Йорке</button>
</div>
);
}// Переиспользуемый компонент модального окна
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>
);
});
// Использование
function App() {
const modalRef = useRef(null);
return (
<div>
<button onClick={() => modalRef.current.open()}>
Открыть модалку
</button>
<Modal ref={modalRef}>
<h2>Содержимое модалки</h2>
<p>Это переиспользуемый компонент модального окна</p>
</Modal>
</div>
);
}// Компонент, которому нужны и внутренний, и переданный ref
const VideoPlayer = React.forwardRef((props, forwardedRef) => {
const internalRef = useRef(null);
// Комбинируем ref'ы
const setRefs = useCallback((node) => {
// Устанавливаем внутренний ref
internalRef.current = node;
// Устанавливаем переданный ref
if (forwardedRef) {
if (typeof forwardedRef === 'function') {
forwardedRef(node);
} else {
forwardedRef.current = node;
}
}
}, [forwardedRef]);
useEffect(() => {
// Используем внутренний ref для логики компонента
if (internalRef.current) {
internalRef.current.volume = 0.5;
}
}, []);
return <video ref={setRefs} {...props} />;
});// Передаем ref разным элементам в зависимости от пропсов
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} />;
});// Компонент высшего порядка, сохраняющий 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>
);
});
// Устанавливаем имя для отладки
WithTooltipComponent.displayName =
`withTooltip(${Component.displayName || Component.name})`;
return WithTooltipComponent;
}
// Использование
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;
}
// Типобезопасный forwardRef компонент
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}
/>
);
}
);
// Использование с TypeScript
function App() {
const buttonRef = useRef<HTMLButtonElement>(null);
const focusButton = () => {
buttonRef.current?.focus();
};
return (
<Button ref={buttonRef} variant="primary">
Нажми меня
</Button>
);
}// Интерфейс кастомного ref handle
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}
/>
);
}
);
// Типобезопасное использование
function App() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="video.mp4" />
<button onClick={() => playerRef.current?.play()}>Играть</button>
<button onClick={() => playerRef.current?.pause()}>Пауза</button>
<button onClick={() => playerRef.current?.seekTo(30)}>
Перейти к 30с
</button>
</div>
);
}// Оптимизированный компонент с memo и forwardRef
const ExpensiveComponent = React.memo(
React.forwardRef((props, ref) => {
console.log('ExpensiveComponent отрендерен');
// Тяжелые вычисления
const result = useMemo(() => {
return heavyCalculation(props.data);
}, [props.data]);
return (
<div ref={ref}>
{result}
</div>
);
})
);
// Кастомная функция сравнения
const OptimizedComponent = React.memo(
React.forwardRef((props, ref) => {
return <div ref={ref}>{props.content}</div>;
}),
(prevProps, nextProps) => {
// Кастомная логика сравнения
return prevProps.content === nextProps.content;
}
);// ❌ Плохо - нет display name
const MyComponent = React.forwardRef((props, ref) => {
return <div ref={ref} />;
});
// ✅ Хорошо - с display name
const MyComponent = React.forwardRef((props, ref) => {
return <div ref={ref} />;
});
MyComponent.displayName = 'MyComponent';// ❌ Неправильно - ref указывает на React компонент, а не DOM
const Card = React.forwardRef((props, ref) => {
return <AnotherComponent ref={ref} />;
});
// ✅ Правильно - ref указывает на DOM элемент
const Card = React.forwardRef((props, ref) => {
return (
<div ref={ref} className="card">
<AnotherComponent />
</div>
);
});const FlexibleComponent = React.forwardRef((props, ref) => {
const setRef = (node) => {
// Обрабатываем оба типа ref
if (ref) {
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
}
};
return <div ref={setRef}>{props.children}</div>;
});ForwardRef — важный инструмент для создания переиспользуемых React компонентов, которым нужно предоставлять доступ к своим внутренним DOM элементам или императивным API родительским компонентам.
Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪