22

React refs使用指南

 3 years ago
source link: http://nakeman.cn/engineering/webprogramming/a-guide-to-react-refs-useref-and-createref.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(本文略译自《 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。

Table of Contents

如何创建 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)。

BZRZFjr.png!mobile

对话框是一个独立了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);

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK