React Ref
引用某个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 | class CustomTextInput extends React.Component { |
当以上CustomTextInput
组件mount时, React会调用ref
回调函数, 当unmount时, 传入参数null
调用. 分别在componentDidMount
, componentDidUpdate
这两个生命周期中.
在React组件内部使用ref
使用是获取DOM元素的常用方式, 以回调的形式(如上例)使用该属性是推荐方式. 更加简洁的推荐使用方式: ref={input => this.textInput = input}
.
在class组件中添加ref
在类组件中使用ref
属性时, ref
回调接受已经mount的组件实例作为其参数. 如果我们想要实现上例中的CustomTextInput
在mount之后立即模拟被点击, 可以这样做:
1 | class AutoFocusTextInput extends React.Component { |
注意CustomTextInput
只有在被声明为class组件时, 这种情况才生效.
1 | class CustomTextInput extends React.Component { |
Refs 和 Functional 组件
因为funtional组件没有实例, 因此不可以在其中使用ref
属性.
1 | function MyFunctionalComponent() { |
不过在functional组件中的DOM元素或class组件中, 可以使用ref
属性
1 | function CustomTextInput(props) { |
将DOM ref暴露给父组件
在有些情况下(但极少), 开发者需要在父组件中获取子组件的DOM节点. 一般来说这种方法是不推荐的, 因为违背了组件封装的原则, 可是在某些情况下, 却不得不用到这种方法, 比如触发聚焦或计算子组件DOM节点的大小与位置.
开发者可以给子组件添加ref
属性(见在class组件中添加ref), 但这并不是理想的解决方式, 因为这种方式只能够获取到组件的实例, 而不是DOM节点, 此外, 如果子组件是functional组件, 这种方法也不会生效.
因此在这种情况下, React推荐开发者在子组件中暴露一个特殊的属性. 该组组件接受一个名为inputRef
的函数属性. React会对其进行处理, 然后将ref
属性添加到该组件下的DOM节点上. 这样, 父组件就能够将ref
回调传给子组件的DOM节点, 见下例:
1 | function CustomTextInput(props) { |
以上例子中, 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 | function CustomTextInput(props) { |
此时, 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 | Warning: |
旧版本:
1 | Warning: |
一般是由以下三种原因导致的:
- 在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 | let app; |
多个React拷贝
Bower在删除重复数据方面处理得很好, 可是npm并不. 如果调试之后发现不是ref
的用法出了问题, 那很可能就是因为项目中加载了多个React的拷贝. 这种问题常常在项目中引入第三方库的时候出现.
在命令行中输入npm ls react
可以查看项目中存在的react版本.