引用某个react UI组件的时候遇到了这样的告警: addComponentAsRefTo(…): Only a ReactOwner can have refs. You might be adding a ref to a component that was not created inside a component’s render method, or you have multiple copies of React loaded.

于是去了解了一下ref的用法:

Refs and the DOM

React的典型数据传递方式是通过props将数据从父组件传给子组件. 一旦props更新, 子组件重新渲染. 但在某些情况下, 开发者会想要不通过props传递数据更新, 而是直接更新子组件. 这些子组件可以是React组件的实例或者是DOM元素. 对于以上这两种子组件, React提供了一种直接更新的方法: 使用refs.

何时使用refs

  • 处理聚焦, 文本选择, 影音播放
  • 触发imperative animation 参考
  • 与第三方DOM库结合使用时

如果操作可以使用说明式方式完成(things can be done declaratively), 尽量使用说明式, 避免使用refs.

比如, 在Dialog组件中, 能够通过传isOpen属性完成的操作, 就不要暴露open(), close()方法执行.

不要滥用refs

开发者很容易滥用refs来处理问题, 但更好的方式是考虑清楚各个组件层级应该拥有的state, 使不同层级的组件拥有不同的state使得应用更加清晰有条理, 易于开发维护. 处理state相关例子

添加ref到DOM元素中

ref是React组件中的一个特殊属性, 该属性接受一个回调函数, 在该组件 mount/unmount 的时候回调函数会立即执行.

ref属性用在HTML元素中时, ref回调函数的参数是该DOM元素. 以下代码中, 使用ref存储了DOM节点(第一个input)的引用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.focusTextInput = this.focusTextInput.bind(this);
}

focusTextInput() {
// 使用DOM API 显式聚焦第一个`input`元素的文本
this.textInput.focus();
}

render() {
// 使用`ref`将DOM节点(第一个`input`)的引用存储到this.textInput
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

当以上CustomTextInput组件mount时, React会调用ref回调函数, 当unmount时, 传入参数null调用. 分别在componentDidMount, componentDidUpdate这两个生命周期中.

在React组件内部使用ref使用是获取DOM元素的常用方式, 以回调的形式(如上例)使用该属性是推荐方式. 更加简洁的推荐使用方式: ref={input => this.textInput = input}.

在class组件中添加ref

在类组件中使用ref属性时, ref回调接受已经mount的组件实例作为其参数. 如果我们想要实现上例中的CustomTextInput在mount之后立即模拟被点击, 可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
class AutoFocusTextInput extends React.Component {
componentDidMount() {
this.textInput.focusTextInput();
}

render() {
return (
<CustomTextInput
ref={(input) => { this.textInput = input; }} />
);
}
}

注意CustomTextInput只有在被声明为class组件时, 这种情况才生效.

1
2
3
class CustomTextInput extends React.Component {
// ...
}

Refs 和 Functional 组件

因为funtional组件没有实例, 因此不可以在其中使用ref属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyFunctionalComponent() {
return <input />;
}

class Parent extends React.Component {
render() {
// 不可以这样做:
return (
<MyFunctionalComponent
ref={(input) => { this.textInput = input; }} />
);
}
}

不过在functional组件中的DOM元素或class组件中, 可以使用ref属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function CustomTextInput(props) {
// textInput 必须在此处声明, 这样ref回调函数才可以引用它
let textInput = null;

function handleClick() {
textInput.focus();
}

return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

将DOM ref暴露给父组件

在有些情况下(但极少), 开发者需要在父组件中获取子组件的DOM节点. 一般来说这种方法是不推荐的, 因为违背了组件封装的原则, 可是在某些情况下, 却不得不用到这种方法, 比如触发聚焦或计算子组件DOM节点的大小与位置.

开发者可以给子组件添加ref属性(见在class组件中添加ref), 但这并不是理想的解决方式, 因为这种方式只能够获取到组件的实例, 而不是DOM节点, 此外, 如果子组件是functional组件, 这种方法也不会生效.

因此在这种情况下, React推荐开发者在子组件中暴露一个特殊的属性. 该组组件接受一个名为inputRef的函数属性. React会对其进行处理, 然后将ref属性添加到该组件下的DOM节点上. 这样, 父组件就能够将ref回调传给子组件的DOM节点, 见下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}

class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}

以上例子中, Parent组件将ref回调通过inputRef属性传给CustomTextInput, CustomTextInput再将该回调以ref属性的方式传给<input>. 最终, 父组件Parent中的this.inputElement的值就是子组件CustomTextInput<input>元素.

注意以上例子中, 属性inputRef没有特殊意义, 就是组件的一般属性. 不过, 必须要在<input>元素本身使用ref属性, 这样React才会将该属性与DOM节点关联.

即使CustomTextInput是functional组件, 以上的例子也会生效. ref是有特定含义的属性, 只能够在DOM元素和class组件中使用, inputRef
没有特定含义, 只是一般的属性, 因此在组件中都可以使用, 没有使用上的限制.

这种模式的另一个优点是: 可以深入多层级的组件. 比如, 在Parent组件中不需要获取其子组件的DOM节点, 但包含Parent组件的GrandParent组件需要获取, 这时也可以使用inputRef来DOM节点. 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}

function Parent(props) {
return (
<div>
My input: <CustomTextInput inputRef={props.inputRef} />
</div>
);
}


class Grandparent extends React.Component {
render() {
return (
<Parent
inputRef={el => this.inputElement = el}
/>
);
}
}

此时, ref回调是在Grandparent组件中首次声明, 通过inputRef一层层传到CustomTextInput, 使得Grandparent组件通过this.inputElement获取到<input>元素.

总的来说, 我们是反对暴露DOM节点的, 但特殊情况总是免不了, 此时就要使用以上的方法. 注意这种方式要求你在子组件中添加一些代码, 如果你无法控制子组件, 还有另外一种方式findDOMNode(), 但不推荐使用这种方式.

过时API: 字符串ref

过去将ref以字符串的形式声明, ref="textInput, 然后通过this.refs.textInput获取相应的DOM节点. 但现在这种方法不被推荐使用, 因为字符串形式的ref会引起一些问题, 所以React团队不推荐这种用法, 在将来的版本中或许会完全抛弃.

一个小问题

如果ref函数以inline函数的方式定义, 它会被调用两次, 第一次的参数是null, 第二次是DOM元素. 这是因为每一次渲染都会创建一个函数的实例, React需要清除旧的ref然后重新设置一个新的ref. 以bound method的方式定义ref回调函数可以避免这个问题, 不过在大部分情况下, 即使以inline函数的方式定义, 也不会引起什么问题.

Refs Must Have Owner

开发过程中遇到了以下警告:

React 16.0.0+ 版本中:

1
2
3
4
5
Warning:

Element ref was specified as a string
(myRefName) but no owner was set.
You may have multiple copies of React loaded. (details:https://fb.me/react-refs-must-have-owner).

旧版本:

1
2
3
4
5
6
7
8
Warning:

addComponentAsRefTo(…):
Only a ReactOwner can have refs.
You might be adding a ref to a component
that was not created inside a component’s
render method,
or you have multiple copies of React loaded.

一般是由以下三种原因导致的:

  • 在fuctionnal组件中添加ref属性
  • 在组件的render()函数之外创建了一个元素, 在该元素中添加了ref属性
  • 加载了多个React的拷贝(其中一个可能原因是npm依赖配置错误)

在funtional组件中添加ref

如果组件<Foo>是functional组件, 不可以这样添加ref:

1
<Foo ref={foo} />

在render方法之外添加值为字符串形式的ref属性

当开发者添加了值为字符串的ref属性到了一个没有父组件的组件中(即该组件不在父组件的render方法内创建)

不可以这样添加 ref:

1
ReactDOM.render(<App ref="app" />, el);

正确方式:

1
2
3
4
5
6
7
let app;
ReactDOM.render(
<App ref={inst => {
app = inst;
}} />,
el
);

多个React拷贝

Bower在删除重复数据方面处理得很好, 可是npm并不. 如果调试之后发现不是ref的用法出了问题, 那很可能就是因为项目中加载了多个React的拷贝. 这种问题常常在项目中引入第三方库的时候出现.

在命令行中输入npm ls react可以查看项目中存在的react版本.

参考