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

0 阅读6分钟

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

前言

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

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


一、useRef 基础概念

1.1 什么是 useRef?

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

import { useRef } from 'react';

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

1.2 useRef 与 useState 的对比

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

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


二、useRef 实战案例

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

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

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。
效果图

屏幕录制 2026-02-26 180409.gif

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

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

错误写法:使用普通变量
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的效果图

屏幕录制 2026-02-26 180755.gif 可以看到此时的Intervalid,并且当我们点击停止时,它会停止

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

屏幕录制 2026-02-26 181130.gif

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

正确写法:使用 useRef
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++,停止按钮都能正确清除定时器。
效果图

屏幕录制 2026-02-26 181550.gif

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


总结

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

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

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