浅谈useRef的使用和渲染机制

前言

刚开始使用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值按钮,组件会重新渲染。

ini 复制代码
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在这两种场景的应用

    • 计时器中使用最新的状态或属性
    ini 复制代码
    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
typescript 复制代码
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 / 内部方法" 的场景)
相关推荐
invicinble3 分钟前
【无标题】
前端
EasyGBS6 分钟前
从接入到稳定播放:无插件直播H5视频流媒体播放器EasyPlayer.js如何撑起Web端流媒体
开发语言·前端·javascript
数智启示录6 分钟前
Apache Kafka Consumer 扩到 40 个仍不提速:Partition 上限锁死有效并行度 【Kafka合集】
数据库·经验分享·分布式·缓存·面试·kafka·apache
不可能片场8 分钟前
nohup 之后进程还是死了:GUI 子进程的存活陷阱
前端·electron
Carl_奕然12 分钟前
【智能体】Agent的四种设计模式之:React(2026最新版)
javascript·人工智能·python·react.js·设计模式·语言模型
数智启示录13 分钟前
Apache Kafka 幂等 Producer 的边界:Exactly-Once 到了 MySQL 为什么失效 【Kafka合集】
数据库·经验分享·分布式·mysql·面试·kafka·apache
名为沙丁鱼的猫72914 分钟前
React/JSX 编码规范
前端·javascript·react.js
是吕先森17 分钟前
【python】selenium实现web自动化测试
前端·python·selenium
Highcharts.js25 分钟前
甘特图纵向任务、横向时间显示图表可视化开发|Highcharts甘特图示例
开发语言·前端·javascript·可视化·甘特图·数据可视化·highcharts
yume_sibai36 分钟前
Element Plus 导航与反馈组件完全指南(15个核心组件)
前端·elementui·交互