3.16 Передача ссылок
Передача ссылки - это метод для автоматической передачи ref
через компонент к
одному из его потомков. Обычно это не требуется для большинства компонентов приложения.
Однако это может быть полезно для их некоторых видов, особенно в библиотеках
компонентов для многоразового использования. Наиболее распространенные сценарии описаны ниже.
Рассмотрим компонент FancyButton
, который отображает нативный
DOM-элемент button
:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
Компоненты React скрывают свои детали реализации, включая результат отрисовки.
Другие компоненты, использующие FancyButton
, обычно не нуждаются в получении
ссылки ref
на внутренний DOM-элемент button
. Это хорошо, так как мешает
компонентам слишком сильно полагаться на DOM-структуру друг друга.
Хотя такая инкапсуляция и желательна для компонентов уровня приложения, таких
как FeedStory
или Comment
, она может оказаться неудобной для часто переиспользуемых
«листовых» компонентов(листья дерева компонентов), таких как FancyButton
или MyTextInput
.
Эти компоненты, как правило, используются во всем приложении аналогичным образом, как и
обычные DOM-компоненты button
и input
, а доступ к их DOM-узлам может быть неизбежным для
управления фокусировкой, выбором(тег select
) или анимацией.
Передача ссылок (ref forwarding) - это дополнительная функция, позволяющая
компонентам передавать получаемую ссылку ref
дальше своему потомку.
В приведенном ниже примере компонент FancyButton
использует React.forwardRef
для получения
переданной ему ссылки ref
и последующей передачи её DOM кнопке button
, которую он отображает:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// Теперь вы можете получить ссылку ref напрямую и передать ее DOM кнопке button:
const myRef = React.createRef();
<FancyButton ref={myRef}>Click me!</FancyButton>;
Таким образом, компоненты, использующие FancyButton
, смогут получить ссылку ref
на DOM-узел button
,
а также, если это необходимо, и доступ к нему, как если бы они напрямую использовали button
.
Приведем пошаговое объяснение того, что происходит в приведенном выше примере:
- Мы создаем React ссылку вызывая
React.createRef
и присваиваем её переменной myRef. - Далее передаем нашу ссылку ниже в
<FancyButton ref={myRef}>
, указывая её как JSX атрибутref
. - React передает ссылку в функцию
(props, ref) => ...
внутриforwardRef
вторым аргументом. - Мы передаем данный аргумент
ref
ниже в<button ref={ref}>
, указывая его как JSX атрибутref
. - Когда ссылка будет присоединена,
ref.current
будет указывать на DOM-узел<button>
.
Внимание!
Второй аргументref
передается только при определении компонента с помощью вызова React.forwardRef
.
Обычные компоненты-функции или компоненты-классы не получают аргумент ref
. Он также недоступен и в свойствах props
.
Передача ссылок не ограничивается DOM-компонентами. Вы также можете передавать ссылки экземплярам компонентов-классов.
Когда вы начнете использовать forwardRef в библиотеке компонентов, вы должны рассматривать это как ломающее изменение и выпускать новую major версию вашей библиотеки. Это связано с тем, что ваша библиотека, вероятно, будет иметь отличающееся поведение (например, из-за того какие ссылки присваиваются и какие типы экспортируются). Это может нарушить работу существующих приложений и других библиотек, которые зависят от вашего старого поведения.
Условное применение React.forwardRef
, если оно существует, также не рекомендуется по тем же
причинам: оно меняет поведение вашей библиотеки и может нарушать работу приложений ваших
пользователей при обновлении самого React.
Также данный подход может быть особенно полезен для
компонентов более высокого порядка
(также известных как HOC).
Начнем с примера HOC, который логирует свойства props
компонента в консоли:
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return LogProps;
}
Старший компонент LogProps
передает все свойства props
компоненту, который он оборачивает,
поэтому отображаемый результат не изменится. Например, мы можем использовать этот HOC для
логирования всех свойств, которые передаются нашему компоненту «fancy button»
:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Вместо экспортирования FancyButton, мы экспортируем LogProps.
// Он по-прежнему будет отрисовывать FancyButton.
export default logProps(FancyButton);
В приведенном выше примере есть один подводный камень: ссылки ref
передаваться не будут.
Это происходит потому, что ref
не является свойством. Подобно ключу key
, ссылка ref
обрабатывается React-ом по-другому.
Если вы добавите ссылку ref
в HOC, она будет ссылаться на самый внешний компонент-контейнер, а не на обернутый компонент.
Это означает, что ссылки, предназначенные для нашего компонента FancyButton
,
будут на самом деле привязаны к компоненту LogProps
:
import FancyButton from './FancyButton';
const ref = React.createRef();
// Компонент FancyButton, который мы импортировали, является
// старшим компонентом LogProps. Даже если отрисованный результат
// будет прежним, наша ссылка ref будет указывать на LogProps,
// вместо вложенного компонента FancyButton! Это означает, что мы не
// можем вызвать, например ref.current.focus()
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
К счастью, мы можем явно передать ссылки на внутренний компонент FancyButton
,
используя API React.forwardRef
. React.forwardRef
принимает render-функцию (функцию отрисовки),
которая получает параметры props
и ref
, и возвращает узел React. Например:
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// Передадим пользовательское свойство "forwardedRef" как ссылку ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// Обратите внимание: второй параметр "ref" предоставлен React.forwardRef.
// Мы можем передать его дальше в LogProps как обычное свойство, например "forwardedRef"
// Затем ссылка может быть присоединена к Component.
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
React.forwardRef
принимает функцию отрисовки (рендеринга). React DevTools использует эту
функцию, чтобы определить, что отображать для компонента, передающего ссылку.
Например, следующий компонент появится как «ForwardRef» в DevTools:
const WrappedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
Если вы как-либо назовете функцию отрисовки, DevTools также добавит её имя (например, «ForwardRef(myFunction)»):
const WrappedComponent = React.forwardRef(
function myFunction(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
);
Вы даже можете указать свойство displayName
для функции,
чтобы добавить компонент, который вы оборачиваете:
function logProps(Component) {
class LogProps extends React.Component {
// ...
}
function forwardRef(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
// Дайте этому компоненту полезное отображаемое имя name в DevTools.
// например "ForwardRef(logProps(MyComponent))"
const name = Component.displayName || Component.name;
forwardRef.displayName = `logProps(${name})`;
return React.forwardRef(forwardRef);
}