React refs使用指南

微信扫一扫,分享到朋友圈

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。

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)。

对话框是一个独立了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>
<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>
<label for={id}>{label}</label>
<input id={id} onChange={onChange} value={value} ref={ref} />
</div>
);
};
export default React.forwardRef(LabelledInput);

微信扫一扫,分享到朋友圈

React refs使用指南

网约美女买了零食就消失:背后套路深似海

上一篇

黄仁勋发布致NVIDIA员工信:ARM继续保持中立

下一篇

你也可能喜欢

React refs使用指南

长按储存图像,分享给朋友