Press "Enter" to skip to content

React refs使用指南

(本文略译自《A guide to React refs: useRef and createRef》)

本文,我们将探究React的一个设计——全面抽象封装了DOM的操作,又开了一个小门给开发者直接操作DOM。

作为新的第二代UI库,React将DOM的操作进行抽象封装,引入了一种新的开发模式——SPA由一个个带状态的 View组件 组成,开发者将 交互功能 理解成 View的状态的变更 的结果[em],这个观念改变是突变的。

EM:View的状态(state)或形式(props)变更都会触发重渲染,都会产生 某种交互输出的效果(交互功能的实现),但性质完成不同。从名字上就可以看出来,一个是交互计算(state),一个交互辅助(props),例如用props去初化一个新子V,会产生交互输出的效果,但不是交互计算。

当我们习惯了这个新模式后会发现,过往的同样的任务(开发交互功能),使用View组件在 分析设计和实现 上都比较原来 DOM命中模式 更好。

不过,React团队非常的有远见,就像其它代码库作者一样,想到了:为特殊情况开一后门(逃生门),这扇“逃生门”就是 Refs。

如何创建 refs

Refs 是React提供的特殊API,通过它,你可以绕过VV抽象,引用到DOM节点,按需要[em]对这些节点进行修改(包括改变某个属性值,或者节点树结构)。

要注意的是,“逃生门”不是正门,能不用尽量不要用,因为它可能会与 React 的自动机制产生冲突,包括diff算法。

EM:什么样需求?操纵DOM节点,是交互功能的技术表现!

误用refs会产生反模式,后面我们会介绍 一些反模式,现在我们先看看如何使用refs去获取一个R组件的DOM节点。

1 类组件与createRef()

import React from 'react'

class ActionButton extends React.Component {

  render() {
    const { label, action } = this.props
    return (
      <button onClick={action}>{label}</button>
    )
  }
}

这个JSX定义里,<button> 只是一个V node,而不是真正的DOM node。想访问button对应的DOM node可以这样:

import React, { createRef } from 'react'

class ActionButton extends React.Component {

  constructor() {
    super()
    this.buttonRef = createRef()
  }

  render() {
    const { label, action } = this.props
    return (
      <button onClick={action} ref={this.buttonRef}>{label}</button>
    )
  }
}

分两步,第一,在类构造器创建一个refs对象实例;第二,在JSX的V node上添加这个refs属性;
使用上,你可在任意的生命事件勾子[em]里 通过 this.buttonRef.current 来访问此DOM node。

EM:为什么是在生命事件勾子呢?

2 Hooks 与 useRef

createRef()必须是用在「React 类组件」,对于 「hook组件」 则有对应的一个hook :useRef

import React, { useRef } from 'react'

function ActionButton({ label, action }) {
    const buttonRef = useRef(null)

    return (
      <button onClick={action} ref={buttonRef}>{label}</button>
    )
  }
}

现在我们已经掌握了 refs 的基础使用,我们接着看看几个 refs 常见使用情景。

React refs常见实例

React 带给前端社区最大的一股潮流,就是 以声明的方式创建 V 对象[em];而在这之前,是命令过程式的时代——交互功能通过DOM指令(或包装成函数)直接“变更”对应的DOM节点属性或结构。

EM:React创新最大的是VV(在原来第一代V的基础之上再进一步),声明只另一部分(事实「声明」只是创建V对象实例的一种新方式,能直观表达两个之间的二维结构关系)。V对象内部还是命令式实现,V的意义在将某个交互功能相关的计算集中在一起,易于分析设计和维护。

正如前面所提到的,我们将交互功能包装一个个V对象,而V对象以是「内部状态的编程」来实现 交互功能的效果 的,它不会触及到物理的DOM结构,React 帮我们决定如何以及何时修改DOM节点产生相对应的交互效果,我们活在一个“牢笼”里。

在实际开发中,有一些交互效果不容易或者不方便表达为 V对象形式或「内部状态的编程」,原始的DOM操作会更有效。我们看看最常见的几种这样的场景。

1 输入控件的聚焦

第一常见例子是表单控件的聚焦(Focus control)。

假设我们有一个订单列表,每一个订单项都有可编辑数量(Quantity)的功能,当我们点击“编辑”按键会弹一个模式编辑对话框(modal)。

对话框是一个独立了V组件(InputModal),代码如下:

import React from "react";

class InputModal extends React.Component {
  constructor(props) {
    super(props);

    this.state = { value: props.initialValue };
  }

  onChange = e => {
    this.setState({ value: e.target.value });
  };

  onSubmit = e => {
    e.preventDefault();
    const { value } = this.state;
    const { onSubmit, onClose } = this.props;
    onSubmit(value);
    onClose();
  };
  render() {
    const { value } = this.state;

    return (
      <div className="modal--overlay">
        <div className="modal">
          <h1>Insert a new value</h1>
          <form action="?" onSubmit={this.onSubmit}>
            <input
              type="text"
              onChange={this.onChange}
              value={value}
            />
            <button>Save new value</button>
          </form>
        </div>
      </div>
    );
  }
}

export default InputModal;

这里,当对话框渲染出来后,如果输入框能立即获得编辑焦点(focus),用户不必使用鼠标手动聚焦,则这个用户体验会非常好。

由于节点的聚焦是通过节点的focus()函数实现的,所以最好的实现方式是使用refs,获取节点的引用,再在适当的时机(InputModal 完成渲染后)执行这个函数即可。

import React, { createRef } from "react";
    
    class InputModal extends React.Component {
      constructor(props) {
        super(props);
        this.inputRef = createRef();
    
        this.state = { value: props.initialValue };
      }
    
      componentDidMount() {
        this.inputRef.current.focus();
      }
    
      onChange = e => {
        this.setState({ value: e.target.value });
      };
    
      onSubmit = e => {
        e.preventDefault();
        const { value } = this.state;
        const { onSubmit, onClose } = this.props;
        onSubmit(value);
        onClose();
      };
    
      render() {
        const { value } = this.state;
    
        return (
          <div className="modal--overlay">
            <div className="modal">
              <h1>Insert a new value</h1>
              <form action="?" onSubmit={this.onSubmit}>
                <input
                  ref={this.inputRef}
                  type="text"
                  onChange={this.onChange}
                  value={value}
                />
                <button>Save new value</button>
              </form>
            </div>
          </div>
        );
      }
    }
    
    export default InputModal;

2 检测节点是否被包含

第二个常见例子,是事件联动。

事件联动 是指交互页某个(VV的)事件会触发页面另一部分的响应这个事件。例如上面的模式对话框,当用户点击 对话框边以外的区域 时,我们希望会关闭这个对话框。对话框边以外的区域 严格上不属于 对框InputModal的,但是这个逻辑与它相关,最好写入它的实现上。

具体实现是:

import React, { createRef } from "react";

class InputModal extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = createRef();
    this.modalRef = createRef();

    this.state = { value: props.initialValue };
  }

  componentDidMount() {
    this.inputRef.current.focus();

    document.body.addEventListener("click", this.onClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("click", this.onClickOutside);
  }

  onClickOutside = e => {
    const { onClose } = this.props;
    const element = e.target;

    if (this.modalRef.current
      && !this.modalRef.current.contains(element)) {
      e.preventDefault();
      e.stopPropagation();
      onClose();
    }
  };

  onChange = e => {
    this.setState({ value: e.target.value });
  };

  onSubmit = e => {
    e.preventDefault();
    const { value } = this.state;
    const { onSubmit, onClose } = this.props;
    onSubmit(value);
    onClose();
  };

  render() {
    const { value } = this.state;
    return (
      <div className="modal--overlay">
        <div className="modal" ref={this.modalRef}>
          <h1>Insert a new value</h1>
          <form action="?" onSubmit={this.onSubmit}>
            <input
              ref={this.inputRef}
              type="text"
              onChange={this.onChange}
              value={value}
            />
            <button>Save new value</button>
          </form>
        </div>
      </div>
    );
  }
}

export default InputModal;
  • 第一,创建一个InputModal边界节点的refs:modalRef;
  • 第二,为文档安装一个全局的点击事件处理函数(onClickOutside);
  • 第三,在onClickOutside中,我们进行判断(例如对 modalRef 进行比较)并执行相应的处理。

这里特别注意两点:

  • 第一,在使用modalRef前先检查它的可用性,因为React动态性很强的;
  • 第二,记得卸载安装的事件处理函数。

3 集成通用代码库

第三个例子,是集成基于DOM的第三方代码库。例如,业界演化了很多成熟的动画库可用,此例子略。

refs 的使用准则

学会 refs 后会发现,实现同一项交互功 能既可用View ,也可以用 refs,这样很容易误用或滥用 refs,造成写出很反模式(anti-pattern)的代码。因为同一个交互功能,直观上好像使用 refs更方便。

我的一条经验准则就是:当你的交互功能需要(以命令式)执行一个API函数,而这个函数没有React对应的API时[em]

EM:这个准则有待进一步归纳

我们看一个很常见的 反模式例子(甚至在面试中也常看到)。

import React, { createRef } from 'react';

class Form extends React.Component {
  constructor(props) {
    super(props)
    this.inputRef = createRef()
  
    this.state = { storedValue: '' }
  }

  onSubmit = (e) => {
    e.preventDefault()
    this.setState({ storedValue: this.inputRef.current.value })
  }  

  render() {

    return (
      <div className="modal">
        <form action="?" onSubmit={this.onSubmit}>
          <input
            ref={this.inputRef}
            type="text"
          />
          <button>Submit</button>
        </form>
      </div>
    )
  }  
}

这是一个典型的使用 refs 访问 非受控组件 的状态值的例子。这个代码是可以运行的。但是,React V抽象API已经实现了对DOM节点的状态(values)和形式属性(properties)的访问,没必要通过refs,正门可入,不必从后门。例如:

render() {
  const { value } = this.state

  return (
    <input
      type="text"
      onChange={e => this.setState({ value: e.target.value })}
      value={value}
    />
  )
}

我们再回顾刚才提到的准则:“当你的交互功能需要(以命令式)执行一个React没有直接抽象的API函数”,再看上面那 非受控组件 的例子,我们创建 了 ref,但并没有 以命令式 执行一个函数来使用这个refs,不像之前的 focus的例子就有。

refs的传递

直到目前为止,我们认识到,refs 对于实现 某种特殊的交互操作 是非常有用的。但是,上面举的例子,相对于实际生产中的代码,则过于简单化。

产品级的V 组件要复杂得多,几乎不会直接用HTML ,都是包装封装结构的自定义组件。例如如下的一个 LabelInput:

import React from 'react'

const LabelledInput = (props) => {
  const { id, label, value, onChange } = props

  return (
    <div class="labelled--input">
      <label for={id}>{label}</label>
      <input id={id} onChange={onChange} value={value} />
    </div>
  )
}

export default LabelledInput

当我们在JSX上给LabelInput定义一个ref,引用到的是这个自定义V的实例,而不是它内部的节点。那么上面的focus的功能就实现不了。

还好 React 提供了另一个特殊的API forwardRef ,我们可以将ref 传入自定义组件的内部:

import React from "react";

const LabelledInput = (props, ref) => {
  const { id, label, value, onChange } = props;

  return (
    <div class="labelled--input">
      <label for={id}>{label}</label>
      <input id={id} onChange={onChange} value={value} ref={ref} />
    </div>
  );
};

export default React.forwardRef(LabelledInput);

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *