「React 技巧」:Refs
摘要
Refs 是 React 提供的一种在典型数据流之外修改子组件的方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。Ref 可以直接操作 DOM 元素,这些操作往往脱离 React 的控制,容易引发 bug。我们需要了解 Ref 的正确使用规范,避免在项目中因为 Ref 造成意想不到的问题。
阅读本文,你可以了解到 Ref 工作原理,React 官方推荐的 Ref 使用方式:forwardRef 和 useImperativeHandle。
介绍
React 典型数据流是从上到下,props 是父子组件交互的唯一方式。要修改一个组件,需要使用新的 props 渲染它。但在某些情况下(如管理焦点,文本选择,媒体播放,触发强制动画等)需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。Refs 直接操作 DOM API,调用 element.focus(),element.remove() ,不受 React 的控制。React 官方将 Ref 称作 escape hatch(逃生窗口),提醒开发这勿过度使用 Refs,而React 希望尽可能的控制 Refs,对 Refs 使用提出规范。React 推荐两种使用 Refs 的方法:
React.createRef
在 Class Compoent 中使用。我们在 constructor 创建了一个 refEle 的对象,在 render 时将 div#classRef 元素的 ref 属性设为 refEle,通过 refEle.current 访问这个 DOM 实例。
import React from "react";
class ClassRefCompt extends React.Component {
constructor() {
super();
this.refEle = React.createRef();
}
componentDidMount() {
console.log("refs:", this.refEle.current);
}
render() {
return (
<div>
<div id="classRef" ref={this.refEle}>
Hello Ref - createRef().
</div>
</div>
);
}
}
export default ClassRefCompt;
useRef()
在 Function Component 中使用,我们直接创建了一个 refEle 的对象,在 render 时将 div#functionRef 元素的 ref 属性设为 refEle,输出 refEle。
import React, { useRef } from "react";
const FunctionRefCompt = () => {
const refEle = useRef();
console.log("function ref:", refEle);
return (
<div>
<div id="functionRef" ref={refEle}>
Hello Ref - useRef().
</div>
</div>
);
};
export default FunctionRefCompt;
控制台的输出告诉我们,第一次 ref.current 是 null,第二次拿到了 DOM 实例。调用 createRef/useRef ,会先创建创建一个对象,初始化对象的 current 为 null,在组件渲染后,React 会把 DOM 节点挂载到对象的 current。在 useEffect 中访问避免 ref.current 不存在。
import React, { useEffect, useRef } from "react";
const FunctionRefCompt = () => {
const refEle = useRef();
console.log("function ref:", refEle);
useEffect(() => {
console.log("in useEffect ref:", refEle);
}, []);
return (
<div>
<div id="functionRef" ref={refEle}>
Hello Ref - useRef().
</div>
</div>
);
};
export default FunctionRefCompt;
不正确使用 Refs 会引发 bug。正如 React 文档里提到的例子,单击按钮 1 将插入/删除 P 节点,单击按钮 2 将调用 DOM API 以删除 P 节点。
import React, { useState, useRef } from "react";
export default function ErrorRef() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => {
setShow(!show);
}}
>
Toggle with setState
</button>
<button
onClick={() => {
ref.current.remove();
}}
>
Remove from the DOM
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
按钮1 移除了 p 元素,按钮2通过 ref 来删除 p 元素。如果这两种删除 p 元素的方式混合使用,那么点击按钮1再点击按钮2会报错。
方法
在 React 中,组件可以分为低阶组件和高阶组件。低阶组件基于 DOM 元素,可以向 DOM 元素传递 Ref,如下面的 MyInput 组件。
const MyInput = (props) => {
const inputRef = useRef(null);
return <input ref={inputRef} {...props} />;
};
高阶组件基于低阶组件封装。高阶组件不能直接向 DOM 元素传递 Ref,例如下面的 MyForm 组件,基于MyInput 组件包装器,我们无法做到单击 MyForm 组件中的按钮来操作输入焦点。
import React, { useRef } from "react";
const MyInput = (props) => {
return <input {...props} />;
};
const MyForm = () => {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>input聚焦</button>
</>
);
};
export default MyForm;
React 为了尽可能控制 Refs,默认不支持跨组件传递 Refs。
forwardRef
我们可以使用 forwardRef API 显式的传递 Refs 来取消限制。在示例中,使用 forwardRef 包裹 MyInput,显式的将 Ref 传递到 DOM 元素中。对于 React 来说,开发者使用 forwardRef 说明他知道自己在做什么,并应该自己承担相应的风险。 同时,forwardRef 的存在更容易开发者定位 Ref 相关的错误。
import React, { forwardRef, useRef } from "react";
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
const MyForm = () => {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>input聚焦</button>
</>
);
};
export default MyForm;
useImperativeHandle
除了限制跨组件 Refs,React 还提供了 useImperativeHandle API,通过限制 Refs 可使用的方法减少错误。useImperativeHandle 应与 forwardRef 一起使用:
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}));
return <input {...props} ref={inputRef} />;
});
MyForm 组件通过 input.current 会以下数据结构,调用其他 DOM API 则会报错。
{
focus: () => {
realInputRef.current.focus();
},
}
结论
Refs 是在 React 典型数据流之外,获取子组件实例的方式。Refs 是 React 的逃生窗口,不受其控制,随意使用 Refs 会造成意想不到的问题。React 提供了 forwardRef API 用于解除 Refs 跨组件传递的限制,useImperativeHandle 限制父组件可调用的 Refs 方法。正确使用 Ref 的方式:通过 forwardRef 关闭 React 限制,useImperativeHandle 限制调用方法,保证程序的健壮性。
代码地址:codesandbox.io/s/winter-cd…
参考
[1] zh-hans.reactjs.org/docs/react-…
[2] zh-hans.reactjs.org/docs/hooks-…
[4] javascript.plainenglish.io/new-react-d…
转载自:https://juejin.cn/post/7186285800442003516