[React翻译]Refs and the DOM

811 阅读4分钟

一般情况在一个正常的dataflow中,父组件总是将data通过props传给子组件,然后子组件re-render。但是有的时候我们想立即修改一个react子组件或一个DOM,那该怎么办呢?

对的,就是用refs

refs提供了一个DOM节点或react组件实例的引用,这样我们在组件中就能访问到那个DOM节点或react组件实例

什么时候用refs

  • 处理focus,text 选择,或者视频回放
  • 触发交互式动画
  • 整合第三方DOM库

不要使用ref去做可以声名的事情。比方说不要让Dialog组件暴露open()和close()方法,而是使用props.isOpen去管理Dialog

创建refs

用React.createRef()创建refs的,然后通过ref属附加给一个React元素或者DOM。在构造组件时,通常将Refs分配给实例属性,以便可以在整个组件中引用它们。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

访问refs

在生命周期方法或者事件处理器中可以使用 current 属性访问ref

const node = this.myRef.current
  • 如果 ref 是给HTML元素用的,那么this.myRef.current 就是一个DOM
  • 如果ref 是给react组件用的,那么this.myRef.current 就是当前组件的实例
  • ref 不能用在函数式组件中,因为函数式组件没有实例

给DOM加ref

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // 1. 首先创建ref
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // 3. 最后,我们在事件处理器中就能使用原生的DOM方法了
    this.textInput.current.focus();
  }

  render() {
    // 2. 然后,告诉react我们希望把构造器里定义的textInput 指向 <input>
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
  1. 组件mount的时候current会被赋值为DOM,当然了还没mount的时候是null。
  2. ref会在componentDidMount or componentDidUpdate 之前更新。

给class组件加ref

比方说我们想要一个AutoFocusTextInput组件,这个组件就是在mount之后调一下CustomTextInput的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型组件

给function组件加ref

render的时候不能给一个function组件加ref

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

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // This will *not* work!
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}

但是一个function组件内部还是能用ref的,只要这个ref不是attach到function组件的

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 refs给父组件

想不想在父组件里面访问一个子DOM节点呢?

不,你不想。

因为这破坏了组件的封装。

但有些时候还是可以用的,比如说触发一个focus或者测量DOM节点的大小和属性。

上面给class组件加ref的例子,有的时候也不是最理想的,因为这个时候返给我们的是一个react组件,而不是一个DOM。

forward refs能让组件选择暴露哪些子组件的refs供自己用。

建议不要暴露DOM节点,但它可以是一个escape hatch。这个方法要向子组件添加一些代码。然后如果我们完全无法控制子组件实现,最后一个选项是使用findDOMNode(),但在StrictMode中是不能用的。

回调refs

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

ref不仅可以使用React.createRef()创建,还可以使用一种叫回调ref的方法实现。我们给render函数里的dom加上一个回调ref,如上面的this.setTextInputRef。在挂载组件的时候react会调用回调ref,然后这个回调的参数会被react认为是当前的dom。这上面的例子中,回调ref,也就是this.setTextInputRef将这个dom的引用赋给了this.textInput。

组件之间可以传回调refs。

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 prop 赋了一个回调,然后传给CustomTextInput,完了CustomTextInput又把这个回调给了input当做一个回调refs。这样的结果是啥呢?是Parent里头的this.inputElement会赋值为一个DOM,而这个DOM恰恰就是CustomTextInput里头的input

有回调refs的注意事项

ref 回调定义为内联函数的话更新的时候会被调用2次,1次refs是null,1次是DOM。这是因为每次渲染都会创建一个新的函数实例,所以react需要先清除旧的ref然后再设置新的ref。

可以把ref callback给bind到class上,不过大多数情况下这个事影响不大