前言
刚开始使用react时,由于对react的hook不太了解,导致在使用useState时,出现了闭包的问题,当时搜索解决方法时,发现了useRef这个hook可以很快的解决这个问题。这里用来记录下自己对useRef这个hook的理解。
useRef的渲染机制
先要了解react中useRef和useState的区别,useState是用来管理组件状态的,而useRef是用来管理组件引用的。useState会导致组件重新渲染,而useRef不会。对应上面说的useRef解决闭包问题,其实不是react设置该hook的初衷,useRef这个hook的初衷是用来解决DOM操作问题的。能够解决闭包问题也只是其副作用之一。对于useRef的渲染机制我们可以总结以下几个关键点:
- useRef在组件首次渲染时创建一个对象 { current: initialValue }
- 整个组件的生命周期,不会创建新对象,返回的都是首次创建对象的引用
- 无论如何赋值,都不会导致组件重新渲染(React通过Object.is比较检测不到变化,因此不会触发渲染)
以下是基于useRef的渲染机制的代码示例,组件使用了antd的Button和Card组件,从代码运行中我们可以看出,点击更新Ref值按钮,组件没有重新渲染,但是点击更新State值按钮,组件会重新渲染。
import { Button, Card } from "antd";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
const DemoRef = () => {
const ref = useRef<any>(null)
// 渲染次数
const renderCountRef = useRef(1);
const renderValueRef = useRef<any>(0);
const [renderValue, setRenderValue] = useState<any>(0);
useEffect(() => {
renderCountRef.current = renderCountRef.current + 1;
console.log(`🔄 组件第 ${renderCountRef.current + 1} 次渲染`);
});
return <div className="flex w-full">
<Card title="useRef的渲染机制" className="ml-12px" hoverable={true}>
<div className="mt-12px max-w-200px text-[#e74c3c] bg-[#fffacd] p-4 text-[18px] font-bold flex w-full flex-row w-400px">
组件渲染此时:<span className="font-bold">{renderCountRef.current}</span>
</div>
<Button onClick={() => {
renderValueRef.current = renderCountRef.current + 1;
}} className="mt-12px">更新Ref值</Button>
<div className="mt-12px font-bold mb-12px">当前Ref值:{renderValueRef.current}(点击虽然新增了,但是组件没有重新渲染,导致此处仍然时老的值)</div>
<Button onClick={() => {
setRenderValue((pre: number) => pre + 1);
}}>更新State值</Button>
<div className="mt-12px font-bold">当前State值:{renderValue}(点击会触发组件重新渲染,导致此处的值会更新)</div>
</Card>
</div>
}
useRef解决的问题
-
解决闭包问题:useRef能够在闭包函数中访问到最新的状态或属性是因为.current属性的引用不会改变。实际编码中以下两个场景会产生闭包,计时器显示和事件函数监听,下面分享下useRef在这两种场景的应用
- 计时器中使用最新的状态或属性
const [duration, setDuration] = useState(0); const durationRef = useRef(duration); useEffect(() => { const interval = setInterval(() => { durationRef.current = durationRef.current + 1; setDuration(durationRef.current); }, 1000); return () => clearInterval(interval); }, []);- 事件处理函数中使用最新的状态或属性,此处不再列举代码和说明,因为和计时器的场景类似。
-
react组件中DOM操作:useRef可以用来操作DOM元素,例如获取输入框的值、滚动到指定位置等。 const inputRef = useRef(null); const handleClick = () => { inputRef.current.focus(); };
-
解决性能问题,方便避免重复创建ref的内容
useRef的好兄弟forwardRef
在日常的开发中,多层组件嵌套是常有的场景,例如父组件中嵌套子组件,子组件中又嵌套孙子组件等。在这种场景下,我们偶尔会需要在父组件中操作子组件的DOM。这时候,如果直接在子组件上使用useRef,会获取不到子组件的DOM并且控制还会报错,原因是react为了保证组件的封装性,默认情况下自定义的组件是不会暴漏其内部DOM节点的ref,具体错误大家可以自己试试。报错提示中会提示我们在子组件中需要使用forwardRef来转发ref。
- 以下是基于forwardRef的代码示例,从代码运行中我们可以看出,点击设置子组件的年龄为18按钮,子组件的年龄会更新为18。
- 使用方式:子组件使用forwardRef包裹,需要转发的方式使用useImperativeHandle
import { Button, Card } from "antd";
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
const LoginForm = forwardRef((props: any, ref: React.ForwardedRef<{ reset: (flag: boolean) => void; }>) => {
const { name } = props;
const [formData, setFormData] = useState({
username: '',
password: ''
});
useImperativeHandle(ref, () => ({
// 重置表单内容
reset: () => {
setFormData({
username: '',
password: ''
})
},
}));
return <div className="mt-12px font-bold">
<h2>{name}</h2>
<div>
<div className="mt-12px">
用户名:<Input type="text" value={formData.username} onChange={(e) => {
setFormData({
...formData,
username: e.target.value
})
}} />
</div>
<div className="mt-12px">
密码:<Input type="password" value={formData.password} onChange={(e) => {
setFormData({
...formData,
password: e.target.value
})
}} />
</div>
</div>
</div>
})
//父组件
<Card title="useRef和useForwardRef的组合" className="ml-12px" hoverable={true}>
<Button onClick={() => {
childRef.current?.reset();
}} type="primary">重置</Button>
<Divider></Divider>
<LoginForm name="登录表单" ref={childRef} />
</Card>
- forwardsRef注意点
- forwardsRef和useRef组合很方便操作子组件的DOM,但是我们尽量避免在父组件中直接操作子组件的DOM,因为这会破坏组件的封装性,导致代码难以维护。
- forwardsRef和useRef组合通过useImperativeHandle转发的方法,我们可以在父组件中控制子组件的状态,但是如非必要此种也尽量少用,优先用 useState + props 传递状态(如父组件通过 isVisible props 控制子组件弹窗),而非用 ref 调用方法(ref 仅用于 “必须操作 DOM / 内部方法” 的场景)