useRef 详解:从自动聚焦到非受控表单,彻底掌握 React 的"持久引用"
在 React 的世界里,useState 是大家耳熟能详的主角------它负责管理状态、驱动界面更新。但还有一个低调却不可或缺的角色:useRef 。它不像 useState 那样会触发重新渲染,却在很多关键场景中默默支撑着应用的正常运行。今天,我们就用生活化的比喻和真实代码,带你彻底理解 useRef 的两大核心用途。
一、什么是 useRef?它和 useState 有什么区别?
✨ 基本定义
jsx
const refContainer = useRef(initialValue);
useRef返回一个可变的引用对象 ,其结构为{ current: initialValue }。- 这个对象在组件的整个生命周期内保持不变(同一个引用)。
- 修改
ref.current不会触发组件重新渲染。
与 useState 对比
| 特性 | useState |
useRef |
|---|---|---|
| 是否可变 | 是(通过 setter 更新) | 是(直接赋值 ref.current = ...) |
| 是否触发重渲染 | ✅ 是 | ❌ 否 |
| 用途 | 管理需要反映在 UI 上的状态 | 存储不需要触发更新的值 / 获取 DOM 元素 |
| 初始值是否参与依赖 | 是(用于 useEffect 等) |
否(.current 变化不会被 React 感知) |
💡
useState是"公告栏"------内容一变,全村都知道;
useRef是"私人笔记本"------你写多少字,别人看不见,但你自己随时能查。
🎯 场景一:让输入框"自动聚焦"------挂载后立刻获得焦点
想象一下你打开一个登录页面,光标已经自动停在用户名输入框里,不用你手动点一下------是不是很贴心?这种体验背后,就离不开 useRef。
来看这段代码:
jsx
import {useRef, useEffect } from 'react';
export default function App() {
const inputRef = useRef(null); // 创建一个"引用盒子"
useEffect(() => {
inputRef.current.focus(); // 页面加载完,立刻聚焦
}, []);
return (
<>
<input ref={inputRef} type="text" />
</>
);
}
运行展示:

🔍 它是怎么工作的?
useRef(null)创建了一个持久存在的对象 ,它的.current属性初始为null。- 当
<input ref={inputRef} />被渲染时,React 会自动把真实的 DOM 元素(比如<input>标签)赋值给inputRef.current。 - 在
useEffect(组件挂载后执行)中,我们调用inputRef.current.focus(),就像对这个输入框说:"嘿,准备好接收输入吧!"
当我们在此基础上添加响应式状态时:
jsx
import {
useState,
useRef ,
useEffect
} from 'react'
export default function App(){
const [count, setCount] = useState(0) // 响应式状态
const inputRef = useRef(null) //初始值为空
console.log(inputRef.current);
console.log(count);
useEffect(() => {
console.log(inputRef.current);
inputRef.current.focus()
}, [])
return(
<>
<input ref={inputRef} type="text" />
{count}
<button type="button" onClick={() => setCount(count + 1)}>增加</button>
</>
)
}
运行程序:

我们可以发现,程序先是输出了ref和count的初始值null和0,此时返回 JSX,React 准备将 <input ref={inputRef} /> 挂载到 DOM。
React 将 JSX 渲染为真实 DOM。 此时,<input> 元素被创建,并且 React 自动将该 DOM 元素赋值给 inputRef.current。输出 < input type="text" >
当我们点击增加按钮时,触发重新渲染,程序输出1和< input type="text" >,为什么useRef的.current不是输出null ,那是因为useRef 的 .current 一旦被 React 赋值,就会一直保留该值,直到组件卸载或手动修改。
📱 适用场景
- 登录/注册页的首字段自动聚焦
- 移动端减少用户点击次数,提升体验
- 表单弹窗打开后自动定位到第一个输入框
总结
useState 和 useRef 的核心区别:useState 管理响应式状态,更新会触发组件重新渲染;而 useRef 创建一个可变且持久的引用对象,其 .current 值在首次渲染时为 null(DOM 尚未挂载),挂载后指向真实 DOM 元素,后续渲染中保持引用不变,且修改它不会引起重渲染。
⏱️ 场景二:存储定时器 ID------避免"失联"的定时任务
再来看一个经典问题:为什么我启动了定时器,却无法停止它?
如果你这样写:
jsx
// 此处省去导入
export default function App(){
let intervalId = null
const [count,setCount] = useState(0)
function start(){
intervalId = setInterval(()=>{
console.log('tick~~~')
},1000)
console.log(intervalId);
}
useEffect(() =>{
console.log(intervalId);
},[count])
function stop(){
clearInterval(intervalId)
}
return(
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>增加</button>
</>
)
当我们点击开始按钮时:

定时器开始每秒打印 'tick~~~',此时组件没有重新渲染 ,点击停止时,clearInterval(id) 成功清除定时器。
而当我们点击增加按钮时,就会出现这种情况:

tick~~~ 持续输出,定时器无法清理! 那是因为当我们点击增加按钮时,组件会重新渲染,React 会重新调用 App 组件函数(即重新渲染),intervalId 都会被重置为 null ,clearInterval(null) 无效, 真正的定时器 ID 已经"丢失" ,无法被清除 → 'tick~~~' 持续输出!
问题在于:每次 count 变化导致组件重新渲染时,let intervalId = null 会被重新执行,之前的定时器 ID 就"丢失"了。
✅ 正确做法:用 useRef 保存 ID
jsx
import { useRef, useState, useEffect } from 'react';
export default function App() {
const intervalId = useRef(null); // ✅ 持久存储
const [count, setCount] = useState(0);
function start() {
intervalId.current = setInterval(() => {
console.log('tick~~~');
}, 1000);
}
function stop() {
clearInterval(intervalId.current);
}
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
{count}
<button onClick={() => setCount(count + 1)}>增加</button>
</>
);
}
🧩 为什么 useRef 能解决?
useRef返回的对象在整个组件生命周期中始终是同一个对象。- 即使组件多次重新渲染,
timerId.current依然保留上次的值。 - 因此,
stop()总能拿到正确的定时器 ID。
上述两个场景体现了useRef 提供一个跨渲染保持不变的可变容器 ,适合存储 DOM 引用或副作用相关的标识(如定时器 ID),且修改它不会引起组件重新渲染。
📝 受控 vs 非受控:表单数据的两种获取方式
React 表单有两种处理思路:受控组件 和 非受控组件 。它们的核心区别在于:谁在掌控表单的值。
1️⃣ 受控组件(Controlled Component)------"一切尽在掌握"
当表单元素的值由 React 状态(state)驱动时,这个表单元素就是一个受控组件
- 表单元素的值由 React state 控制。
- 必须配合
onChange更新状态。
jsx
import { useState } from 'react';
export default function LoginForm() {
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = (e) => {
setForm({
...form,
[e.target.name]: e.target.value // 动态更新字段
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form); // { username: '...', password: '...' }
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={form.username}
onChange={handleChange}
placeholder="请输入用户名"
/>
<input
name="password"
value={form.password}
onChange={handleChange}
placeholder="请输入密码"
type="password"
/>
<button type="submit">注册</button>
</form>
);
}
受控组件的核心原则
"有
value,必有onChange。"否则输入框会被 React "锁住",用户无法输入
如果没有onChange,会发生什么?
- 初始时
form.username = '',输入框为空 ✅ - 用户输入 "alice" → 浏览器尝试把输入框值改为 "alice"
- 但 React 在渲染时又强制把
value设回''(因为form.username没变!) - 结果:输入框"卡住",用户无法输入任何内容! ❌
为什么选择受控组件?
- 数据完全受控,便于校验、格式化、联动(如确认密码)
- 符合 React 单向数据流理念
2️⃣ 非受控组件(Uncontrolled Component)------"用时再取"
- 表单元素自己管理值(像传统 HTML)。
- 通过
useRef在需要时读取.current.value。
jsx
import { useRef } from 'react';
export default function CommentBox() {
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current.value;
if (!comment) return alert('请输入评论');
console.log(comment);
};
return (
<div>
<textarea ref={textareaRef} placeholder="输入评论..." />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
- 优点:性能略高(无状态更新),代码更简洁。
- 适用场景:评论框、文件上传、一次性提交的简单表单。
核心区别:表单数据由谁"掌控"?
| 对比维度 | 受控组件(Controlled) | 非受控组件(Uncontrolled) |
|---|---|---|
| 数据来源 | React 的 state |
DOM 元素自身(原生 HTML 行为) |
| 如何更新值 | 通过 onChange 同步到 state |
用户直接操作 DOM,React 不干预 |
| 如何读取值 | 直接读取 state |
通过 ref.current.value 获取 |
是否需要 value + onChange |
✅ 必须配对使用 | ❌ 不需要(通常只用 ref) |
| 是否触发 re-render | 每次输入都触发 | 无状态变化,不触发 |
| 适合场景 | 需要实时校验、格式化、联动的复杂表单(如登录、注册、设置页) | 一次性提交、简单输入(如评论框、搜索框、文件上传) |
| 优点 | - 数据流清晰 - 易于验证/转换/联动 - 符合 React 响应式理念 | - 代码简洁 - 性能略高(无频繁 setState) - 接近原生 HTML 习惯 |
| 缺点 | - 代码量稍多 - 频繁输入可能引发多余渲染(可通过防抖优化) | - 无法实时响应输入 - 难以实现动态校验或字段联动 - 违背"状态驱动 UI"原则 |
✨ 总结:useRef 的核心价值
| 用途 | 说明 | 示例 |
|---|---|---|
| 1. 访问 DOM 元素 | 获取真实 DOM,调用原生方法 | .focus(), .scrollIntoView() |
| 2. 持久存储可变值 | 保存不触发重渲染的数据 | 定时器 ID、WebSocket 实例 |
| 3. 构建非受控组件 | 一次性读取表单值 | 评论框、文件上传 |
记住一句话:
需要界面跟着变?用
useState。
只想悄悄存个东西或操作 DOM?用useRef。
useRef 虽不张扬,却是 React 开发中不可或缺的"幕后英雄"。
掌握它,你就能在"优雅的 React"和"灵活的 DOM"之间自由切换,写出既健壮又高效的代码!