Refs and the DOM

在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。如果需要修改子组件,可以通过修改 props 重新渲染。

然而,在有些情况下,除了使用典型的交互模式外,需要强制的修改子组件。要修改的子组件可能是React的示例,也可能是DOM元素,而React提供了一种可以同时作用于上述两种情况的解决方案。

什么时候使用Refs

下面是几个使用 Refs 的示例:

  • 管理焦点状态(focus)、文本选择(text selection)、或者是媒体播放(media)

  • 强制触发动画

  • 与第三方的DOM库集成

在任何能够通过直接声明完成的事情中应当避免使用Refs。

例如,对于一个Dialog组件,应当提供一个isOpenprop来控制它,而不是暴露open()close()两个方法去操作。

不要过度使用Refs

可能你会倾向于在app中使用Refs来 “触发某些事件” ,如果是这样的话,请花点儿时间思考一下,在不同的组件层次结构中的state应当如何去分发(state的层次结构设计)。

通常来说,拥有satte的组件的层次结构应当较高(通过层次高的组件,将state通过props分发到子组件中)。

可以到Lifting State Up文档来查看示例。

在DOM元素上使用Ref

React提供了一个可以加到任何组件上面的特殊属性,ref属性使用了一个回调函数,并且回调函数在组件挂载(mounted)或者是销毁(unmounted)后立即执行。

ref 属性作用于 HTML 元素上时,ref的回调函数接收底层的DOM元素作为其参数,例如,下面代码中,使用ref回调存储DOM节点的引用。

  1. class CustomTextInput extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.focusTextInput = this.focusTextInput.bind(this);
  5. }
  6. focusTextInput() {
  7. // Explicitly focus the text input using the raw DOM API
  8. this.textInput.focus();
  9. }
  10. render() {
  11. // Use the `ref` callback to store a reference to the text input DOM
  12. // element in an instance field (for example, this.textInput).
  13. return (
  14. <div>
  15. <input
  16. type="text"
  17. ref={(input) => { this.textInput = input; }} />
  18. <input
  19. type="button"
  20. value="Focus the text input"
  21. onClick={this.focusTextInput}
  22. />
  23. </div>
  24. );
  25. }
  26. }

组件挂载是,React将DOM元素作为参数并且调用ref回调函数,而在卸载时将null作为参数调用ref的回调。

通过ref回调函数指定组件类中的某个属性为DOM元素是常见的使用方式,首选的方式是上面示例中的回调函数,当然还有更简短的使用方式:ref={input => this.textInput = input}.

在组件类上使用 Ref

当在声明为类的自定义组件上使用 ref 属性的时候,ref回调函数接收组件的已挂载实例作为回调的参数。

例如,如果我们想要在CustomTextInput组件上面模拟该组件挂在后立即触发点击事件:

  1. class AutoFocusTextInput extends React.Component {
  2. componentDidMount() {
  3. this.textInput.focusTextInput();
  4. }
  5. render() {
  6. return (
  7. <CustomTextInput
  8. ref={(input) => { this.textInput = input; }} />
  9. );
  10. }
  11. }

需要注意的是,仅当CustomTextInput被声明为class的时候的时候才起作用:

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

Refs 和 Function构造的组件

你不应该将 ref 属性作用域 Function 声明的组件,因为没有实例:

  1. function MyFunctionalComponent() {
  2. return <input />;
  3. }
  4. class Parent extends React.Component {
  5. render() {
  6. // This will *not* work!
  7. return (
  8. <MyFunctionalComponent
  9. ref={(input) => { this.textInput = input; }} />
  10. );
  11. }
  12. }

如果你要使用 ref ,需要将组件转 class 定义的组件,就像使用生命周期的方法的时候一样,都需要将其转成 class 定义的组件。

当然,在 function 声明的组件中使用 ref 的话,按照正常的使用方式即可。

  1. function CustomTextInput(props) {
  2. // textInput must be declared here so the ref callback can refer to it
  3. let textInput = null;
  4. function handleClick() {
  5. textInput.focus();
  6. }
  7. return (
  8. <div>
  9. <input
  10. type="text"
  11. ref={(input) => { textInput = input; }} />
  12. <input
  13. type="button"
  14. value="Focus the text input"
  15. onClick={handleClick}
  16. />
  17. </div>
  18. );
  19. }

将 DOM Refs 暴露给父组件

在极少数情况,你可能将子组件的DOM节点传递给父组件。通常来讲不建议这样做,因为会破坏组件的封装。但是偶尔情况下,对于触发焦点或者得到子组件的DOM节点的大小或者位置很有帮助。

虽然你可以向子组件增加一个ref属性,但是并不是理想的解决方案,因为获取到的是组件实例而不是DOM节点。

此外,这种方式对于 funcion 定义的组件无效。

在这种情况下,我们建议通过子组件暴露 ref 属性。子组件通过prop(如父组件传递过来的 inputRef)作为ref的属性添加到子组件的DOM节点上。从而实父组件能够通过中间的 inputRef 来使用子组件的DOM节点。

这种方法对于 class 声明的组件和 function 声明的组件都适用:

  1. function CustomTextInput(props) {
  2. return (
  3. <div>
  4. <input ref={props.inputRef} />
  5. </div>
  6. );
  7. }
  8. class Parent extends React.Component {
  9. render() {
  10. return (
  11. <CustomTextInput
  12. inputRef={el => this.inputElement = el}
  13. />
  14. );
  15. }
  16. }

在上面的例子中,Parent把它的ref回调作为一个inputRef prop 传递给了 CustomTextInput,并且CustomTextInput把回调函数传递给了<input>.因此,Paren中的this.inputElement便是CustomTextInput组件中<input>节点。

值得注意的是,上面示例中inputRef并没有什么特殊的意义,只是一个正常的prop。不过,<input>上的red属性很重要,因为它告诉 React 通过一个ref链接到<input>节点。

即使CustomTextInput是通过 fucntion 定义的组件也依旧能够正常工作(译者注:上面的示例就说明了这一点)。原因在于,和 ref属性不同(ref属性只能作用于class声明的组件,而不能作用于function声明的组件),inputRef只是一个普通的prop,两种类型声明的组件都能够使用。

这种方式的另一个优点在于能够很好的作用于多重嵌套的组件。假设Parent不需要input这个DOM节点,但是Parent的父组件(如:Grandparent)需要使用<input>DOM节点。这事,可以通过为Grandparent指定一个inputRef,然后再通过ParentinputRef传递到CustomTextInput.

  1. function CustomTextInput(props) {
  2. return (
  3. <div>
  4. <input ref={props.inputRef} />
  5. </div>
  6. );
  7. }
  8. function Parent(props) {
  9. return (
  10. <div>
  11. My input: <CustomTextInput inputRef={props.inputRef} />
  12. </div>
  13. );
  14. }
  15. class Grandparent extends React.Component {
  16. render() {
  17. return (
  18. <Parent
  19. inputRef={el => this.inputElement = el}
  20. />
  21. );
  22. }
  23. }

上述例子中,首先在Grandparent声明一个ref的回调,然后传递给了Parent组件,传递的方式是使用了一个inputRef的prop。然后Parent组件将这个prop以同样的名称inputRef传递给了CustomTextInput。最后CustomTextInput组件读取inputRef参数,并将传递的函数作为ref的属性加到<input>节点上。所以Grandparent中的this.inputElement指向CustomTextInput<input>节点。

考虑到所有可能遇到的情况,还是建议尽量不要暴露DOM的节点,但是这确实是一种有效的处理方法。当然,需要注意的是,这种方法需要向子组件中增加一些代码。

如果你无法控制子组件的实例,还可以通过findDOMNode()来作为最后的可选方案,不过不鼓励使用这种方法。

之前版本的API:Refs字符串

如果是使用之前版本React,可能会熟悉一个比较旧的ref的API,将ref属性值设置为一个字符串,比如:textInput,可以通过this.refs.textInput来操作DOM节点。

我们建议尽量不要使用这种方式,因为字符串的refs会导致一些遗留的问题,很可能在之后的版本中,移除该API

如果你正在使用this.refs.inputText去访问refs,我们建议你使用新的回调模式来替代。

注意事项

如果ref回调函数被定义为一个内敛的函数,组件更新时会被调用两次,第一次参数是null,第二次参数是DOM节点。

这是因为每次渲染穿件一个新的函数实例,因此React需要清除之前的引用,然后设置新的引用。

可以通过将ref回调函数定义为类上的绑定方法来避免此情况。不过,多数情况下,问题不大。