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 表单处理的两大模式:受控组件非受控组件,敬请期待!

相关推荐
不会敲代码12 小时前
React 受控组件与非受控组件完全指南
前端·react.js
恋猫de小郭2 小时前
Android 官方正式官宣 AI 支持 AppFunctions ,Android 官方 MCP 和系统级 OpenClaw 雏形
android·前端·flutter
孟陬2 小时前
Tanstack Start 的天才创新之处——基于实际使用体验
react.js·visual studio code·next.js
Moment2 小时前
一周重写 Next.js?Cloudflare 和 AI 做到了😍😍😍
前端·javascript·后端
CodeSheep3 小时前
同事去年绩效是C,提离职领导死活不让走,后来领导私下说他走了,就没人背这个绩效了
前端·后端·程序员
摸鱼的春哥3 小时前
春哥的Agent通关秘籍12:本地RAG实战(中下)向量化与落库
前端·javascript·后端
明月_清风3 小时前
毫秒级响应:前端本地搜索的“降维打击”
前端·indexeddb
摸鱼的春哥3 小时前
专家实验让AI做战争决策,AI的选择太暴力了
前端·javascript·后端
明月_清风3 小时前
存储配额:用 navigator.storage.estimate() 预判浏览器什么时候会删你的数据
前端·indexeddb