Hooks 函数之 useRef 与 useState,受控组件与非受控组件
大家好呀~ 今天咱们来聊聊 React 中非常重要的两个 Hooks 以及表单处理的两种方式 ------useRef、useState,还有受控组件与非受控组件。这些可是 React 开发中的基础又核心的知识点哦,掌握它们能让咱们的代码更优雅、更高效~ 😊
一、useRef 📌
1. 什么是 useRef
useRef 是 React 中用于持久化数据(跨组件渲染周期保存值)和直接操作 DOM 元素的 Hook,核心作用是 "在组件生命周期内保持一个可变的引用"。简单说,它就像一个 "保险箱",可以存东西,而且不管组件怎么重新渲染,这个 "保险箱" 本身不会变,里面的东西却能随时拿出来用~
2. useRef 的特性
- 返回值 :调用
useRef(initialValue)会返回一个不可变的容器对象(ref 对象),该对象只有一个属性current,用于存储实际值。 - 跨渲染周期保持引用:useRef 返回的 ref 容器对象在组件的整个生命周期内引用地址不变(即使组件多次重新渲染,它还是同一个对象)。
- 修改 current 不会触发重渲染 :直接修改
refContainer.current的值,不会导致组件重新渲染,这是它与 useState 的核心区别。
代码举例:
咱们来看 App2.jsx 中的例子:
jsx
javascript
import { useRef, useState } from 'react'
export default function App() {
const [count, setCount] = useState(0); // 响应式状态,修改会触发重渲染
console.log('组件渲染了...'); // 每次count变化都会打印,证明重渲染
// 创建ref对象,初始值为null
const inputRef = useRef(null);
console.log('ref容器对象:', inputRef); // 每次渲染打印的都是同一个对象(引用不变)
// 组件挂载完成后执行(类似vue的onMounted)
useEffect(() => {
console.log('input DOM元素:', inputRef.current); // 此时current已指向input元素
inputRef.current.focus(); // 操作DOM,让输入框自动聚焦
}, []);
return (
<>
{/* 将ref对象绑定到input元素 */}
<input ref={inputRef} />
<p>count: {count}</p>
{/* 点击按钮修改count,触发组件重渲染 */}
<button onClick={() => setCount(count + 1)}>count ++</button>
</>
)
}
代码效果详解:
- 当点击
count ++按钮时,count状态变化会触发组件重渲染,控制台会打印 "组件渲染了..."。 - 但每次打印的
inputRef都是同一个对象(引用地址不变),这体现了 "跨渲染周期保持引用" 的特性。 useEffect钩子在组件挂载后执行,此时inputRef.current已经指向了 input DOM 元素,通过focus()方法实现了自动聚焦,展示了 useRef 操作 DOM 的能力。- 即使多次修改
inputRef.current(比如手动修改它的值),也不会触发组件重渲染,这和 useState 完全不同~
注意观察,光标自动聚焦于输入框、组件渲染了而inputRef容器对象始终不变、修改inputRef.current的值不会触发重新渲染:

3. useRef 与 useState 比较 💡
| 特性 | useRef | useState |
|---|---|---|
| 本质 | 用于存储 "持久化的可变值"(非响应式) | 用于管理组件的响应式状态 |
| 触发重渲染 | 修改 current 不会触发重渲染 |
调用 setXxx 会触发重渲染 |
| 跨渲染周期 | 容器对象引用不变,current 可修改 |
每次渲染会创建新的状态变量(值可能相同) |
| 适用场景 | 操作 DOM、存储定时器 ID、缓存数据等 | 管理影响 UI 展示的状态(如表单输入、开关状态等) |
hooks函数中的useState 在我前面的文章里讲解过,不清楚或者感兴趣的小伙伴可以去翻出来看看。
咱们再看一个对比案例:
jsx
javascript
// App.jsx(用普通变量存定时器ID,失败案例)
export default function App() {
let intervalId = null; // 每次渲染都会重置为null
const [count, setCount] = useState(0);
function start() {
intervalId = setInterval(() => { console.log('tick~~~'); }, 1000);
}
function stop() {
clearInterval(intervalId); // 无法停止!因为intervalId已被重置
}
return <>{/* 按钮省略 */}</>;
}
注意观察:点击开始按钮,系统自动分配一个ID值,点击停止,可以停止因为此时ID值未被修改;再次点击开始按钮,点击count++,然后再点击停止,此时无法停止,因为ID因count值修改触发useState响应式状态修改导致重新渲染而被重置了。

javascript
// App2.jsx(用useRef存定时器ID,成功案例)
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); // 成功停止!因为current保留了定时器ID
}
return <>{/* 按钮省略 */}</>;
}
注意观察:点击开始按钮,系统自动分配一个ID值,点击停止,可以停止因为此时ID值未被修改;再次点击开始按钮,点击count++,然后再点击停止,此时亦可以停止,因为useRef 声明的ID不会因组件渲染而重置,即ID值仍未被修改。

这个例子完美体现了两者的区别:普通变量会在组件重渲染时重置,而 useRef 的 current 能持久化存储值,这就是为什么管理定时器、计时器这类跨渲染周期的值时,useRef 是更好的选择~
二、受控组件与非受控组件 📝
在 React 中,表单元素(如 input、textarea、select 等)的状态管理方式分为受控组件和非受控组件,两者的核心区别在于状态由谁来管理。
1. 受控组件(Controlled Components)
(1)定义
组件的状态(value)由 React 的状态(useState)管理,表单元素的值完全受 React 控制。就像老板(React 状态)直接指挥员工(表单元素),员工做什么都得听老板的~
(2)工作原理
- 通过
value属性将 React 状态与表单元素绑定(单向绑定)。 - 通过
onChange事件监听输入变化,实时更新 React 状态。 - 表单元素的值始终与 React 状态保持一致,形成 "数据驱动视图"。
(3)特点
- 状态完全由 React 控制 :表单的值始终和
value状态保持一致,不会出现 "失控" 的情况。 - 实时可操作性 :可以随时通过修改
value状态来改变表单值(比如重置输入、强制填写格式等)。 - 适合场景:需要实时验证(如输入长度限制)、表单联动(如一个输入框变化影响另一个)、实时展示输入内容的场景。
2. 非受控组件(Uncontrolled Components)
(1)定义
组件的状态由 DOM 自身管理,React 不直接控制表单元素的值,而是通过 ref 访问 DOM 元素获取值。就像老板(React)不直接指挥员工(表单元素),但需要时会去 "查岗"(通过 ref 获取值)~
(2)工作原理
- 使用 useRef 创建一个 ref 对象,绑定到表单元素。
- 不通过 onChange 实时更新状态,而是在需要时(如提交表单)通过
ref.current.value读取 DOM 中的值。
(3)特点
- 状态由 DOM 管理:输入的值直接存在 DOM 中,React 不跟踪实时变化。
- 按需获取值:只有在需要时(如提交、点击按钮)才通过 ref 读取值,减少了状态更新的频率。
- 适合场景:一次性读取值(如表单提交)、性能敏感场景(避免频繁状态更新)、文件上传(input type="file" 必须用非受控方式)。
3. 受控组件与非受控组件比较 🆚
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 状态管理者 | React 状态(useState) | DOM 自身 |
| 获取值方式 | 直接从状态变量获取 | 通过 ref.current.value 获取 |
| 实时性 | 实时更新状态,可即时响应 | 不实时更新,按需读取 |
| 适用场景 | 实时验证、表单联动、动态反馈 | 一次性提交、文件上传、性能优化 |
如何选择?
- 大部分场景优先用受控组件,因为它能更好地体现 React "数据驱动" 的思想,状态可控性更强。
- 当需要操作文件(file input)、追求性能(减少状态更新)或只需要一次性获取值时,用非受控组件更合适。
4.实战演练:
废话不多说,先看代码:
jsx
javascript
// 从 React 库中导入所需的两个 Hook:
// useState:用于创建和管理组件的响应式状态
// useRef:用于创建 DOM 元素引用,实现对原生 DOM 的直接访问
import {
useState,
useRef
} from 'react'
// 定义并导出 App 函数式组件,作为整个表单的根组件
export default function App() {
// 初始化受控组件的响应式状态
// value:存储第一个输入框的输入内容,初始值为空字符串
// setValue:更新 value 状态的方法,调用后会触发组件重渲染,同步更新关联的 UI
const [value, setValue] = useState('');
// 创建一个 ref 引用对象,初始值为 null
// 后续将绑定到第二个输入框,用于获取该输入框的原生 DOM 元素
const inputRef = useRef(null);
// 定义表单提交的处理函数,e 是浏览器原生的表单提交事件对象
const doLogin = (e) => {
// 阻止浏览器表单提交的默认行为(默认会刷新页面,破坏 React 单页应用的运行状态)
e.preventDefault();
// 通过 ref 引用对象的 current 属性,获取第二个输入框的 DOM 元素,读取其 value 属性(用户输入内容)并打印
console.log(inputRef.current.value);
}
// 渲染组件的 UI 结构
return (
// 表单标签,绑定 onSubmit 事件,提交时触发 doLogin 处理函数
<form onSubmit={doLogin}>
<div>
{/* 实时渲染受控组件的状态值 value,直观展示输入内容与 React 状态的同步效果 */}
{value}
{/* 第一个输入框:受控组件(由 React 状态管理输入值) */}
<input
type="text" // 输入框类型为文本输入
value={value} // 将 React 状态 value 绑定到输入框的 value 属性,让状态控制输入框的显示内容
onChange={(e) => setValue(e.target.value)} // 绑定输入变化事件
// 输入变化时,获取输入框当前值(e.target.value),调用 setValue 更新 React 状态 value
// 实现「输入变化 → 更新状态 → 更新 UI」的闭环,保持状态与输入框内容一致
/>
{/* 第二个输入框:非受控组件(由 DOM 自身管理输入值) */}
{/* 不绑定 value 属性和 onChange 事件,仅通过 ref 属性绑定 inputRef,用于后续获取 DOM 元素的值 */}
<input type="text" ref={inputRef} />
</div>
{/* 表单提交按钮,type="submit" 点击后会触发表单的 onSubmit 事件 */}
<button type="submit">登录</button>
</form>
)
}
代码效果解释:
这段代码在一个简单的登录表单中,同时实现了受控组件 和非受控组件两种 React 表单处理模式:
其中第一个输入框是受控组件,它通过 useState 定义响应式状态 value,以 value={value} 实现 React 状态对输入框显示值的单向绑定,又通过 onChange 事件监听输入变化,实时调用 setValue(e.target.value) 将 DOM 输入值同步到 React 状态中,输入框的所有状态完全由 React 管控,还能通过页面上渲染 {value} 实时预览输入内容。
而第二个输入框是非受控组件,它不依赖 React 状态管理,仅通过 useRef 创建的 inputRef 绑定到 DOM 元素,输入内容直接存储在 DOM 自身的 value 属性中,不在输入过程中做实时状态同步(代码中的非受控组件仅通过 ref 绑定 DOM 元素,输入内容只存储在 DOM 自身的 value 属性中,没有触发任何 React 状态更新,也不会触发组件重渲染,因此无法直接像受控组件那样实时展示输入内容。),仅在表单提交(触发 doLogin 函数)时,通过 inputRef.current.value 按需读取 DOM 中的输入值,同时表单提交时通过 e.preventDefault() 阻止了浏览器默认的页面刷新行为,清晰对比了 React 两种表单处理方式的核心差异与实现逻辑。

三、面试官会问 🤔
- useRef 和 useState 的核心区别是什么? 答:useRef 存储的是 "非响应式" 的持久化值,修改
current不会触发重渲染;useState 管理的是响应式状态,调用 setter 会触发重渲染。useRef 适合存储跨渲染周期的非 UI 相关数据(如 DOM、定时器 ID),useState 适合管理影响 UI 的状态。 - 为什么 useRef 能跨组件渲染周期保存值? 答:因为 useRef 返回的 ref 容器对象在组件整个生命周期内引用地址不变,即使组件重新渲染,这个对象也不会被重新创建,所以
current属性存储的值能被持久化。 - 受控组件和非受控组件的区别是什么?分别适合什么场景? 答:受控组件由 React 状态管理值,通过
value和onChange绑定,适合实时验证、表单联动;非受控组件由 DOM 管理值,通过 ref 获取,适合一次性读取、文件上传等场景。 - 如何用 useRef 操作 DOM 元素? 答:先用
useRef(null)创建 ref 对象,然后通过ref属性绑定到 DOM 元素,当组件挂载后,ref.current就会指向该 DOM 元素,进而可以调用 DOM 方法(如focus()、scrollIntoView())。
四、结语 🌟
今天咱们详细学习了 useRef、useState 以及受控组件和非受控组件的知识。useRef 就像一个 "持久化的工具箱",帮我们存储跨渲染周期的数据和操作 DOM;而受控与非受控组件则是表单处理的两种思路,各有适用场景。
其实这些知识点并不难,关键是多动手实践~ 比如用受控组件做一个带实时验证的登录表单,用 useRef 实现一个自动聚焦的搜索框,相信练习之后大家会理解得更透彻!😘