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 的对比
| useRef | useState | |
|---|---|---|
| 返回值 | 一个包含 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。
效果图
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在重新渲染时被重置为null,stop函数中使用的intervalId已经不是之前保存的定时器 ID 了。
原因:函数组件每次渲染都会重新执行内部代码,普通变量 intervalId 每次都被重新赋值为 null,之前保存的 ID 丢失了。
我们先看去掉onClick与useState的效果图
可以看到此时的Intervalid,并且当我们点击停止时,它会停止
我们再看当我加上onclick与useState的效果图
可以看到当我们点击开始的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++,停止按钮都能正确清除定时器。
效果图
我们可以看到当外貌点击开始时的Intervalid为11,并且当我再点击count++时,它的Intervalid依然为 11,这个时候再点击停止,计算器就停止了
总结
本文我们深入学习了 React 中的 useRef Hook,理解了它与 useState 的本质区别:useRef 用于持久化保存数据但不会触发渲染,而 useState 则负责响应式视图更新。通过两个实战案例(自动聚焦输入框和保存定时器 ID),我们掌握了 useRef 最常见的两种使用场景——操作 DOM 和避开闭包陷阱保存跨渲染周期数据。
正确使用 useRef 能够让我们在函数组件中灵活地处理那些不需要重新渲染的“副作用”数据,写出更加健壮和高效的代码。
接下来,我们将进一步探讨 React 表单处理的两大模式:受控组件 与 非受控组件,敬请期待!