React Hooks 进阶:useRef 核心用法与受控/非受控组件实战解析

React Hooks 进阶:useRef 核心用法与受控/非受控组件实战解析

前言

在 React 开发中,我们经常需要直接操作 DOM(如聚焦输入框)或保存跨渲染周期不变的数据(如定时器 ID)。与此同时,表单作为前端交互的核心,其数据管理方式直接影响代码的简洁性与性能。

本文将从零开始,深入剖析 useRef 的核心用法,并通过两个实战案例展示它在 DOM 操作与闭包陷阱中的应用。随后,我们将系统对比受控组件与非受控组件的区别,帮助你在实际项目中做出合适的技术选型。文章配有可运行代码示例,确保理论与实践紧密结合。


一、useRef 基础概念

1.1 什么是 useRef?

useRef 是 React 提供的一个 Hook,它可以返回一个可变的 ref 对象 。这个对象在组件的整个生命周期内持续存在,并且其 .current 属性可以被修改,但修改不会触发组件重新渲染。

jsx 复制代码
import { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(initialValue);
  // 使用 myRef.current 读取或修改
}

1.2 useRef 与 useState 的对比

useRef useState
返回值 一个包含 current 属性的对象 一个状态值和一个更新函数
修改方式 直接修改 current 属性 调用 setter 函数
是否响应式 否,修改不会触发重新渲染 是,修改会触发组件重新渲染
适用场景 保存不参与渲染的数据(如 DOM 引用、定时器 ID) 保存直接影响视图渲染的数据

简单记忆 :useState 负责视图更新 ,useRef 负责数据持久存储但不影响视图。


二、useRef 实战案例

2.1 案例一:访问 DOM 元素(自动聚焦)

最常见的 useRef 用法就是获取 DOM 元素。下面这个例子展示了如何在组件挂载后让输入框自动获得焦点,并通过控制台输出观察 ref 的变化。

jsx 复制代码
import { useState, useRef, useEffect } from 'react';

export default function AutoFocusInput() {
  // 声明一个响应式状态 count,用于演示组件更新
  const [count, setCount] = useState(0);
  // 每次组件更新时都会打印,帮助我们观察渲染次数
  console.log('组件更新了。。。。。。。');

  // 创建一个 ref 对象,初始值为 null,用于关联 input 元素
  const inputRef = useRef(null);
  // 刚创建时 inputRef.current 是 null,打印看一下
  console.log('初次渲染时 ref 的值:', inputRef.current);

  // 自动聚焦:useEffect 在组件挂载后执行
  useEffect(() => {
    // 此时 inputRef.current 已经指向真实的 input DOM 节点
    console.log('useEffect 中 ref 的值:', inputRef.current);
    // 调用 DOM 元素的 focus() 方法,让输入框获得焦点
    inputRef.current.focus();
  }, []); // 空依赖数组表示只在组件挂载时执行一次

  return (
    <>
      {/* 通过 ref 属性将 input 节点与 inputRef 关联起来 */}
      <input ref={inputRef} />
      <p>{count}</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        count++
      </button>
    </>
  );
}

运行观察:

  • 初次渲染时,控制台会先打印"组件更新了。。。。。。。"和"初次渲染时 ref 的值:null"。
  • 然后 useEffect 执行,打印"useEffect 中 ref 的值:"后面跟着真实的 <input> 元素对象,并且输入框自动获得焦点。
  • 点击 count++ 按钮,组件重新渲染,控制台再次打印"组件更新了。。。。。。。",但 useEffect 因为依赖数组为空,不会再次执行,所以输入框不会重新聚焦(符合预期)。

关键点:

  • useRef 在多次渲染之间保持不变,但它的 current 属性在组件挂载后会被 React 赋值为真实的 DOM 节点。
  • 我们可以通过 ref 属性将 React 元素与 ref 对象关联,之后就能直接操作 DOM。
效果图

2.2 案例二:保存定时器 ID(避开闭包陷阱)

很多初学者会尝试用普通变量来保存定时器 ID,但这样在组件重新渲染时变量会被重置,导致无法清除定时器。下面我们通过一个例子来感受这个问题的严重性。

错误写法:使用普通变量
jsx 复制代码
import { useState } from 'react';

export default function Timer() {
  // 错误:用普通变量保存定时器 ID
  let intervalId = null; // 每次组件重新渲染时,这个变量都会被重新赋值为 null
  const [count, setCount] = useState(0);

  const start = () => {
    // 启动定时器,并将 ID 存入 intervalId
    intervalId = setInterval(() => {
      console.log('tick~~~~~~');
    }, 1000);
  };

  const stop = () => {
    // 尝试清除定时器,但 intervalId 可能已经不是当初的那个 ID 了
    clearInterval(intervalId);
  };

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </>
  );
}

现象:

  • 点击「开始」后,控制台每秒输出一次 tick~~~~~~。
  • 点击「count++」按钮,count 增加,页面重新渲染。
  • 此时再点击「停止」,定时器无法停止 ,因为 intervalId 在重新渲染时被重置为 null,stop 函数中使用的 intervalId 已经不是之前保存的定时器 ID 了。

原因 :函数组件每次渲染都会重新执行内部代码,普通变量 intervalId 每次都被重新赋值为 null,之前保存的 ID 丢失了。

我们先看去掉onClick与useState的效果图

可以看到此时的Intervalid,并且当我们点击停止时,它会停止

我们再看当我加上onclick与useState的效果图

可以看到当我们点击开始的Intervalid,再点击count++时,可以看到此时Intervalid变为了null,而且点击停止也不会停止

正确写法:使用 useRef
jsx 复制代码
import { useState, useRef } from 'react';

export default function Timer() {
  // 使用 useRef 保存定时器 ID,它在多次渲染之间保持不变
  const intervalIdRef = useRef(null);
  const [count, setCount] = useState(0);

  const start = () => {
    // 将定时器 ID 存入 ref 的 current 属性
    intervalIdRef.current = setInterval(() => {
      console.log('tick~~~~~~');
    }, 1000);
  };

  const stop = () => {
    // 从 ref 中取出 ID 并清除定时器
    clearInterval(intervalIdRef.current);
  };

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>count++</button>
    </>
  );
}

关键点:

  • useRef 返回的对象在组件的整个生命周期内保持不变,即使组件重新渲染,intervalIdRef.current 仍然指向之前保存的值。
  • 因此,无论点击多少次 count++,停止按钮都能正确清除定时器。
效果图

我们可以看到当外貌点击开始时的Intervalid为11,并且当我再点击count++时,它的Intervalid依然为 11,这个时候再点击停止,计算器就停止了


总结

本文我们深入学习了 React 中的 useRef Hook,理解了它与 useState 的本质区别:useRef 用于持久化保存数据但不会触发渲染,而 useState 则负责响应式视图更新。通过两个实战案例(自动聚焦输入框和保存定时器 ID),我们掌握了 useRef 最常见的两种使用场景------操作 DOM 和避开闭包陷阱保存跨渲染周期数据。

正确使用 useRef 能够让我们在函数组件中灵活地处理那些不需要重新渲染的"副作用"数据,写出更加健壮和高效的代码。

接下来,我们将进一步探讨 React 表单处理的两大模式:受控组件 与 非受控组件,敬请期待!

相关推荐
库拉镜像AI牛牛1 小时前
短剧内容自动化生产:知漫剧工作室落地教程
大数据·服务器·前端·人工智能·语音识别
律宏阔1 小时前
Flutter 列表图片内存优化:CachedNetworkImage 与 ClipRRect 的取舍
前端·flutter
行者全栈架构师1 小时前
【鸿蒙心迹】相册文搜图工程化落地:HarmonyOS 7.0 端侧 AI 检索全链路实战与 5 个踩坑解法
前端
律宏阔1 小时前
Dart 3 Record:解决 Future.wait 异构返回值的类型问题
前端·flutter
花间相见1 小时前
【后端开发|Redis进阶01】—— Redis分布式锁全解:从SETNX到Redisson看门狗,再到RedLock
后端·面试
Seraphina361 小时前
DVWA(XSS Reflected/Stored High,CSRF Low)
前端·xss·csrf
默_笙2 小时前
🍔 中间件不只是打日志:四个钩子、一次短路,和自带的工具
前端·javascript
程序员老赵3 小时前
Docker 部署 Dolibarr:轻松搭建开源 ERP/CRM 平台
运维·前端·后端
星禾元亨3 小时前
同一件事有三份资料,AI 该信哪一份?冲突判定、优先级链与处置流程
前端·人工智能
Dovis(誓平步青云)3 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑