引用Refs提供了一个访问render()方法DOM节点或者React元素的方法。
典型的React数据流中,props
是双亲组件和子组件交互的唯一手段。要修改一个子组件,你需要使用新的props
重新渲染它。然而,确实存在少数一些情况你需要命令性地(imperatively)修改一个子节点而不是通过典型的props
数据流方式。被修改的子节点可能是一个React组件实例,亦或是一个DOM元素。对于这两种情况,React都提供了应急处理方案(escape hatch)。
何时使用引用Refs?
有一些场景很适合使用refs
:
- 管理焦点,文本选择,或者媒体播放
- 触发命令性动画
- 和第三方DOM库集成
除此之外,如果能通过声明方式解决问题,尽量避免使用refs
。
比如,一个Dialog
组件有open()
,close()
暴露出来可用的方法,但是尽量不要去使用它们,而是通过声明的方式传递一个isOpen
这样的属性给它来控制该对话框的打开和关闭。
不要滥用引用Refs
你的第一倾向可能是在你的应用中使用refs
“先把事情搞定”。如果是这样,还请三思,整个组件层级结构中,状态到底应该在哪里被管理呢?很明显,”拥有”(own)属性的合适位置通常都是层级结构中更高层的一些地方。看看这篇文章”状态提升(Lifting State Up)“,有一些相关的例子。
注意:
下面的例子更新了,使用了Ract 16.3引入的API React.createRef()。如果你使用的比较旧的React版本,我们建议你使用refs回调方式(译注:本文后面部分有讲述)。
创建引用Refs
引用Refs通过React.createRef()
创建,通过属性ref
附加到React元素上。Refs通常在一个组件构造时赋值给一个实例属性,这样在整个组件中他们都可以被引用到。例子:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
访问引用Refs
render
方法中,当一个引用ref
被传递给一个元素,在ref
的current
属性中就会出现对该节点的一个引用:
// 继续了上面的例子,目前 node 就指向了上面返回的DOM div 节点
const node = this.myRef.current;
根据节点类型的不同,ref
的值也不同:
- 如果
ref
用在HTML元素上,构造函数中通过React.createRef()
创建的ref
会将底层DOM元素放到它的current
属性中。 - 如果
ref
用在自定义组件类型上,ref
使用它的current
属性指向所挂载的组件实例。 - 功能性组件上不能使用
ref
,因为它们没有实例。
下面的例子演示了这些不同。
为DOM元素增加Ref
引用
下面的代码使用一个ref
保存对一个DOM节点的引用:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
组件挂载时,React会将ref
的current
属性设置成DOM元素,卸载时,再把ref
的current
属性设置为null
。ref
更新发生在componentDidMount
或者componentDidUpdate
生命周期回调之前。
向类组件增加引用Ref
如果我们想封装上面的CustomTextInput
组件模拟它挂载后立即被点击的事件,我们可以使用引用ref
访问这个自定义输入框并手动调用它的focusTextInput
方法:
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
注意,这种方式仅在CustomTextInput
被定义为一个类的情况下才工作:
class CustomTextInput extends React.Component {
// ...
}
引用Refs
和功能性组件
功能性组件上不能使用ref
因为它们没有实例:
function MyFunctionalComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 不工作!不工作!不工作!
return (
<MyFunctionalComponent ref={this.textInput} />
);
}
}
这种情况下,如果你要是用ref
,需要将该组件转换成一个类组件,就跟你需要生命周期方法或者状态时做的一样。
然而,在一个功能性组件内部你可以使用ref
引用属性只要它指向一个DOM元素或者一个类组件。
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
let textInput = React.createRef();
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
暴露DOM引用给双亲组件
在很少的一些情况下,你需要从双亲组件中访问某个子DOM节点。一般来说不建议这么做,因为它打破了组件封装,但是它偶尔也很有用,比如触发获取焦点,或者测量一个子DOM节点的尺寸或者位置。
尽管你可以给子组件增加一个ref
引用(译注:参考上面AutoFocusTextInput
和CustomTextInput
的例子),但这不是一个理想的解决方案,因为你得到的只是一个组件实例而不是一个DOM节点。另外,这个方法对于功能性组件不工作。
如果你在使用React 16.3或者更高版本,我们建议在这些情况下你使用引用转发方案。引用转发允许组件选择将任何子组件的引用ref
当成是自己的。你可以在引用转发文档中找到如何将子节点的DOM节点公开给父组件的详细示例。
如果你使用的是React 16.2或更低版本,或者你需要比引用转发更多的灵活性,那你可以使用这种替代方案,并显式传递引用,属性名称不必是ref
。
可能的话,我们建议不要公开DOM节点,但它有可能是一个有用的应急逃生舱。注意这种方法需要你向子组件添加一些代码。如果你完全不能控制子组件的实现,你最后的选择是使用findDOMNode(),另外一种不被建议使用的方案。
引用回调函数
React支持另外一种设置引用的方法叫做“引用回调函数”,该方法能对引用设置的做更多细粒度的控制。
这种方法中不再是传递一个createRef()
创建的ref
引用,而是传递一个函数。这个函数接收一个React组件实例或者一个HTML DOM元素作为它的参数,用于在其他地方保存或者访问。
下面的例子实现了这一通用模式:使用ref
回调保存一个DOM节点的引用到一个实例属性中去。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// Focus the text input using the raw DOM API
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// autofocus the input on mount
this.focusTextInput();
}
render() {
// Use the `ref` callback to store a reference to the text input DOM
// element in an instance field (for example, this.textInput).
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React会在组件挂载时在DOM元素上调用ref
回调函数(参数就是这个DOM元素),在组件卸载时再次调用该ref
回调函数,使用的参数是null
。ref
回调调用发生在在componentDidMount
或者componentDidUpdate
生命周期回调方法调用之前。
你可以像传递React.createRef()
创建的对象引用那样在组件之间传递引用回调函数:
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
上面的例子中,类Parent
将它的引用回调函数作为一个inputRef
属性传给了子组件CustomTextInput
,然后子组件CustomTextInput
将同一个函数作为ref
属性传给了<input>
。结果就是,Parent
中,this.inputElement
会被设置成CustomTextInput
里面相应的<input>
的元素。
遗留API:字符串引用Refs
如果你之前使用React,可能会对一个比较旧的API比较熟悉,这个API中ref
属性是一个字符串,像是textInput
,而DOM节点通过this.refs.textInput
来访问。我们现在建议不要再使用该方案,因为它有些问题,这个方案因此也已经作为遗留方案被废弃了,很可能在将来的某个版本中会被移除。
使用引用回调函数的注意事项
如果ref
回调函数定义在内联函数(inline function)中,更新时他会被调用两次,第一次参数是null
,第二次参数才是DOM元素。这是因为每个渲染都会创建一个新的函数实例,所以React需要清除旧的引用并设置新的。你可以通过将引用回调定义为该类的绑定方法来避免这种情况,但请注意,大多数情况下这样做或者不这样做都没太大关系。