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节点的引用。

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // Explicitly focus the text input using the raw DOM API
    this.textInput.focus();
  }

  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={(input) => { this.textInput = input; }} />

        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

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

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

在组件类上使用 Ref

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

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

class AutoFocusTextInput extends React.Component {
  componentDidMount() {
    this.textInput.focusTextInput();
  }
  render() {
    return (
      <CustomTextInput
        ref={(input) => { this.textInput = input; }} />
    );
  }
}

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

class CustomTextInput extends React.Component {
  // ...
}

Refs 和 Function构造的组件

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

function MyFunctionalComponent() {
  return <input />;
}

class Parent extends React.Component {
  render() {
    // This will *not* work!
    return (
      <MyFunctionalComponent
        ref={(input) => { this.textInput = input; }} />
    );
  }
}

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

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

function CustomTextInput(props) {
  // textInput must be declared here so the ref callback can refer to it
  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 Refs 暴露给父组件

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

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

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

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

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

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 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.


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}
      />
    );
  }
}

上述例子中,首先在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回调函数定义为类上的绑定方法来避免此情况。不过,多数情况下,问题不大。


在 Github 共同编辑

Github系列文章: