React refs使用指南
2023 review:
- ref 相对于 state就像CSS的 NF,Position 相对 于Flexbox。Flexbox 对完成一常规任务(UI 动态布局)实在太好用了,但也有不适用的地方,至少 Position会更合适。同样,React state V 也很好用,但也不宜用来模仿所有的 交互操作。
- 前端 UI交互 在技术上 都是 DOM(BOM)编程 ——用JS 命令操作 文档对象模型,只是有相当一部分(或一类交互)功能适用 状态(双向绑定)自动实现。
- 理解「交互功能」——分交互计算和交互辅助——是关键,交互计算几乎都适用 **状态响应(双向绑定)**表达,交互辅助则比较适用 直接操作DOM 实现(而像BOM根本不能用状态表达)
- 使用ref 的准则:“当你的交互功能需要(以命令式)执行一个React没有直接抽象的DOM API函数时”
(本文略译自《A guide to React refs: useRef and createRef》)
本文,我们将探究React的一个设计——全面抽象封装了DOM的操作,同时,又开了一个小门给开发者直接操作DOM。
作为新的第二代UI库,React将DOM的操作进行抽象封装,引入了一种新的开发模式——SPA由一个个带状态的 View组件 组成,开发者将 交互功能 理解成 View的状态的变更 的结果,这个观念改变是突变的。
当我们习惯了这个新模式后会发现,过往的同样的任务(开发交互功能),使用View组件在 分析设计和实现 上都比较原来 DOM命中模式 更好。
不过,React团队非常的有远见,就像其它代码库作者一样,想到了:为特殊情况开一后门(逃生门),这扇“逃生门”就是 Refs。
如何创建 refs
Refs 是React提供的特殊API,通过它,你可以绕过VDOM抽象,引用到DOM节点,按需要对这些节点进行修改(包括改变某个属性值,方法,或者节点树结构)。
要注意的是,“逃生门”不是正门,能不用尽量不要用,因为它可能会与 React 的自动机制产生冲突,包括diff算法。
误用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属性; 使用上,你可在任意的生命事件勾子里 通过 this.buttonRef.current 来访问此DOM node。
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 组件 **;而在这之前,是命令过程式的时代——交互功能通过DOM指令(或包装成函数)直接“变更”对应的DOM节点属性或结构。
正如前面所提到的,我们将交互功能包装一个个V对象,而V对象以是「内部状态的编程」来实现 交互功能的效果 的,它不会触及到物理的DOM结构,React 帮我们决定如何以及何时修改DOM节点产生相对应的交互效果,我们活在一个“牢笼”里。
在实际开发中,有一些交互效果不容易或者不方便表达为 V对象形式或「内部状态的编程」,原始的DOM操作会更有效。我们看看最常见的几种这样的场景。
1 输入控件的聚焦
第一常见例子是表单控件的聚焦(Focus control)。
假设我们有一个订单列表,每一个订单项都有可编辑数量(Quantity)的功能,当我们点击“编辑”按键会弹一个模式编辑对话框(modal)。
对话框是一个独立了V组件(InputModal),代码如下:
import React, { createRef } from "react";
class InputModal extends React.Component {
constructor(props) {
super(props);
this.inputRef = createRef();//// 1
this.state = { value: props.initialValue };
}
componentDidMount() {
this.inputRef.current.focus();//// 3
}
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} <!--## 2 ### -->
type="text"
onChange={this.onChange}
value={value}
/>
<button>Save new value</button>
</form>
</div>
</div>
);
}
}
export default InputModal;
这里,当对话框渲染出来后,如果输入框能立即获得编辑焦点(focus),用户不必使用鼠标手动聚焦,则这个用户体验会非常好。
由于节点的聚焦是通过节点的 focus()函数 实现的,所以最好的实现方式是使用refs,获取节点的引用,再在适当的时机(InputModal 完成渲染后)执行这个函数即可。
2 检测节点是否被包含
第二个常见例子,是事件联动。
事件联动 是指交互页某个(VV的)事件会触发页面另一部分的响应这个事件。例如上面的模式对话框,当用户点击 对话框边以外的区域 时,我们希望会关闭这个对话框。对话框边以外的区域 严格上不属于 对框InputModal的,但是这个逻辑与它相关,最好写入它的实现上。
具体实现是:
import React, { createRef } from "react";
class InputModal extends React.Component {
constructor(props) {
super(props);
this.inputRef = createRef();
this.modalRef = createRef(); //// 1
...
}
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)) { //// 3
e.preventDefault();
e.stopPropagation();
onClose();
}
};
...
render() {
const { value } = this.state;
return (
<div className="modal--overlay">
<div className="modal" ref={this.modalRef}> //// 2
<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 的使用准则
“当你的交互功能需要(以命令式)执行一个React没有直接抽象的DOM API函数,可用 ref”
学会 refs 后会发现,实现同一项交互功 能既可用View ,也可以用 refs,这样很容易误用或滥用 refs,造成写出很反模式(anti-pattern)的代码。因为同一个交互功能,直观上好像使用 refs更方便。
我的一条经验准则就是:当你的交互功能需要(以命令式)执行一个API函数,而这个函数没有React对应的API时。
我们看一个很常见的 反模式例子(甚至在面试中也常看到)。
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);
参考
- The Complete Guide to useRef() and Refs in React https://dmitripavlutin.com/react-useref-guide/