Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。
何时使用Refs
在某些特定的场景,必须使用ref来访问DOM元素。
- input的focus、文本select、媒体播放器。
- 触发强制动画。
- 集成第三方 DOM 库。
避免使用 refs 来做任何可以通过声明式实现来完成的事情。
举个例子,避免在 Dialog 组件里暴露 open() 和 close() 方法,最好传递 isOpen 属性。
React提倡不要过度使用ref,除非遇到input的focus类似必须DOM的操作的场景,我们才会使用ref,否则应该使用state和props解决问题,脱离复杂的DOM操作。
使用ref的三大原则:
- 可以在dom元素上面使用ref属性,ref表示对DOM元素节点引用。
- 可以在class组件上面使用ref属性,ref表示对React组件的实例引用。
- 在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件:
- 不能在函数组件上面使用ref属性,原因:函数组件没有实例,父组件获取子组件的ref,其实是在获取子组件的实例;
React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例;需要区分一下,ReactDOM.render()渲染组件时返回的是组件实例;而渲染dom元素时,返回是具体的dom节点。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>Hello Component</div>
);
}
}
const renderComponent = ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
console.log('render组件:', renderComponent)
const renderDom= ReactDOM.render(
<div>Hello Dom</div>,
document.getElementById('root')
);
console.log('render的dom节点:', renderDom)
输出结果为:
render组件: MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, textInput: {…}, …}
render的dom节点: <div>Hello Dom</div>
创建和获取ref
在reactd的版本历史上,出现了三种创建ref的方式:string ref,callback ref,React.createRef。无论哪种方式,都是为ref属性赋值,ref和key一样,都是关键字,为React内部使用。 另外,值得注意的是,所有的ref获取,最好在组件加载结束之后,否则无法获取值。因为组件加载后,dom才准备好了是吧。
string ref 使用
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myRef.focus();
}
render() {
return <input ref="myRef" />;
}
}
callback ref 使用
class MyComponent extends React.Component {
componentDidMount() {
this.myRef.focus();
}
render() {
return <input ref={(ele) => { this.myRef = ele; }} />;
}
}
React 支持给任意组件添加特殊属性。ref属性接受一个回调函数,它在组件被加载或卸载时会立即执行。
- 当给 HTML 元素添加 ref 属性时,ref 回调接收了底层的 DOM 元素作为参数。
- 当给组件添加 ref 属性时,ref 回调接收当前组件实例作为参数。
- 当组件卸载的时候,会传入null ref 回调会在componentDidMount 或 componentDidUpdate 这些生命周期回调之前执行。
关于回调 refs 的说明
如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。
在组件发生了更新时,ref都会重新创建。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的
class Test extends React.Component {
componentDidMount(){
// 获取
console.log(this.second);
// <input value="second">
}
createRef = (dom) => {
this.second = dom;
}
render() {
// 创建
return <input value="second" ref={this.createRef} />
}
}
React.createRef 使用
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Child extends React.Component{
render(){
return <div>This is child</div>
}
}
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount(){
console.log(this.myRef.current)
}
render() {
return (
<div><Child ref={this.myRef}/></div>
);
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例。
此处示例是react16.3+版本的使用(如果想使用旧版本请看下文),步骤有三个:
- createRef方法 生成ref 对象
- render的时候 接收 子组件或者dom元素的ref属性
- 用this.inputRef.current 来获取这个 子组件或者dom元素
将 DOM Refs 暴露给父组件
在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。
虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。
如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档中有一个详细的例子。
Refs 转发
注意:ref不能通过props传递,那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。
Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。最常见的案例如下所述。
React.forwardRef
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
console.log(this.textInput.current)
}
render() {
return (
<FancyButton ref={this.textInput}>Click me!</FancyButton>
);
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
使用FancyButton的组件可以获取底层DOM节点button的ref,并在必要时访问,就像其直接使用 DOM button 一样。
以下是对上述示例发生情况的逐步解释:
- 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
- 我们通过指定 ref 为 JSX 属性,将其向下传递给
<FancyButton ref={ref}>。 - React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
- 我们向下转发该 ref 参数到
<button ref={ref}>,将其指定为 JSX 属性。 - 当 ref 挂载完成,ref.current 将指向
<button>DOM 节点。
注意
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
在高阶组件中转发refs
这个技巧对高阶组件(也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 HOC 示例开始:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
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;
}
const LogPropsHoc = logProps(FancyButton)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
console.log(this.textInput.current)
}
render() {
return (
<LogPropsHoc ref={this.textInput} label='Click Me'>Click me!</LogPropsHoc>
);
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
“logProps” HOC 透传(pass through)所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:
上面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。
这意味着用于我们 FancyButton 组件的 refs 实际上将被挂载到 LogProps 组件:
LogProps {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: FiberNode, …}
context: {}
props: {label: "Click Me", children: "Click me!"}
refs: {}
state: null
updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
_reactInternalFiber: FiberNode {tag: 1, key: null, stateNode: LogProps, elementType: ƒ, type: ƒ, …}
_reactInternalInstance: {_processChildContext: ƒ}
isMounted: (...)
replaceState: (...)
__proto__: Component
幸运的是,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件。React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。例如:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
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} />;
});;
}
const LogPropsHoc = logProps(FancyButton)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
console.log(this.textInput.current)
}
render() {
return (
<LogPropsHoc ref={this.textInput} label='Click Me'>Click me!</LogPropsHoc>
);
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
注意 React.forwardRef 回调的第二个参数 “ref”。我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”,然后它就可以被挂载到被 LogProps 包裹的子组件上。
<button class="FancyButton">Click me!</button>
Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref,Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
总结
React提供ref的初衷是给开发者一个可获取实际DOM的工具。 你可能首先会想到使用 refs 在你的 app 中“让事情发生”。如果是这种情况,请花一点时间,认真再考虑一下 state 属性应该被安排在哪个组件层中。通常你会想明白,让更高的组件层级拥有这个 state,是更恰当的。查看 状态提升 以获取更多有关示例。
参考文章
官网refs使用::zh-hans.reactjs.org/docs/refs-a…
官网refs转发:zh-hans.reactjs.org/docs/forwar…