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 在重新渲染时被重置为 nullstop 函数中使用的 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 表单处理的两大模式:受控组件非受控组件,敬请期待!

相关推荐
ZC跨境爬虫8 小时前
模块化烹饪小程序开发日记 Day6:(菜谱列表接口开发与日志调试实践)
前端·javascript·css·ui·微信小程序·html
programhelp_8 小时前
Roblox Coding OA 面经分享|题量不小,但整体更偏工程思维
人工智能·算法·面试
JAVA社区8 小时前
Java进阶全套教程(一)—— 数据框架Mybatis详解
java·开发语言·面试·职场和发展·mybatis
KaMeidebaby8 小时前
卡梅德生物技术快报|适配体筛选技术架构演进:SPARK-seq 高通量平台原理与技术流程解析
大数据·前端·其他·百度·架构·spark·新浪微博
王璐WL8 小时前
【C++进阶】多态,坑很多,面试常考!!!
c++·面试
JAVA社区8 小时前
Java进阶全套教程(八)—— Docker超详细实战详解
java·运维·开发语言·docker·容器·面试·职场和发展
ZC跨境爬虫8 小时前
跟着 MDN 学CSS day_7:(层叠优先级与继承)
前端·css·数据库·ui·html
Shadow(⊙o⊙)8 小时前
qt信号和槽链接的接入与断开
开发语言·前端·c++·qt·学习
慕斯fuafua8 小时前
JS——DOM操作
前端·javascript·html
忆林5208 小时前
Jenkins前端打包构建老项目拯救指南
运维·前端·jenkins