React Refs 和 DOM
Refs
React.forwardRef
转发
Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧( 使用 ref 暴露 DOM 节点给父组件 )
示例
FancyButton
使用 React.forwardRef
来获取传递给它的 ref
,然后转发到它渲染的 DOM button
:
// 子组件
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 父组件
function App() {
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
// 或者
// const ref = useRef(null);
return (
<>
<FancyButton ref={ref}>Click me!</FancyButton>;
</>
)
}
这样,使用 FancyButton
的组件可以获取底层 DOM 节点 button
的 ref ,并在必要时访问,就像其直接使用 DOM button
一样。
以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.createRef
或useRef
创建了一个 React ref 并将其赋值给ref
变量。 - 我们通过指定
ref
为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
。 - React 传递
ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。 - 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为 JSX 属性。 - 当 ref 挂载完成,
ref.current
将指向<button>
DOM 节点。
在高阶组件中转发 refs
这个技巧对 高阶组件 (也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 HOC 示例开始:
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 HOC 透传(pass through)所有 props
到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 fancy button 组件的 props:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);
下面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref
不是 prop 属性。就像 key
一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。
这意味着用于我们 FancyButton
组件的 refs 实际上将被挂载到 LogProps
组件:
import FancyButton from './FancyButton';
const ref = React.createRef();
// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}/>;
幸运的是,我们可以使用 React.forwardRef
API 明确地将 refs 转发到内部的 FancyButton
组件。 React.forwardRef
接受一个渲染函数,其接收 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;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />; }
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; });}
在 DevTools 中显示自定义名称
React.forwardRef
接受一个渲染函数。React DevTools 使用该函数来决定为 ref 转发组件显示的内容。
例如,以下组件将在 DevTools 中显示为 “ForwardRef”:
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} />;
}
// 在 DevTools 中为该组件提供一个更有用的显示名。
// 例如 “ForwardRef(logProps(MyComponent))”
const name = Component.displayName || Component.name; forwardRef.displayName = `logProps(${name})`;
return React.forwardRef(forwardRef);
}
Refs & DOM
Refs 提供了一种方式,允许我们访问 DOM 节点 或在 render
方法中创建的 React 元素
适合使用 refs 的三种情况
下面是几个适合使用 refs 的情况:
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
避免使用 refs 来做任何可以通过声明式实现来完成的事情。
举个例子,避免在 Dialog
组件里暴露 open()
和 close()
方法,最好传递 isOpen
属性。
如何使用
一、 React.createRef()
- Refs 是使用
React.createRef()
创建的,并通过ref
属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。 - 当 ref 被传递给
render
中的元素时,对该节点的引用可以在 ref 的current
属性中被访问 - 不能在函数组件上使用
ref
属性 ,因为他们没有实例。如果要在函数组件中使用ref
,你可以使用forwardRef
(可与useImperativeHandle
结合使用),或者可以将该组件转化为class
组件。
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 会在组件挂载时给 current
属性传入 DOM 元素,并在组件卸载时传入 null
值。
ref
会在 componentDidMount
或 componentDidUpdate
生命周期钩子触发前更新。
二、回调 Refs
React 支持另一种设置 refs 的方式,称为“ 回调 refs ”。它能助你更精细地控制何时 refs 被设置和解除。
不同于传递 createRef()
创建的 ref
属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 将在组件挂载时,会调用 ref
回调函数并传入 DOM 元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 refs 一定是最新的
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
在上面的例子中, Parent
把它的 refs 回调函数当作 inputRef
props 传递给了 CustomTextInput
,而且 CustomTextInput
把相同的函数作为特殊的 ref
属性传递给了 <input>
。结果是,在 Parent
中的 this.inputElement
会被设置为与 CustomTextInput
中的 input
元素相对应的 DOM 节点。
注意:
如果 ref
回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null
,然后第二次会传入参数 DOM 元素。
这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class
的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论