在 React 的函数式组件(Functional Components)世界里,我们习惯了用 useState 来驱动一切。数据变了 -> 视图更新,这很“响应式”。
但有时候,我们并不想让每一次数据的变动都惊动视图(Re-render),或者我们仅仅想像在 Vue 里 onMounted 拿 DOM 那样简单粗暴地操作一个 input。这时候,useState 就显得有点“大材小用”甚至由于频繁渲染带来性能负担。
今天我们来聊聊 React Hooks 里的“隐形富豪”—— useRef,以及它引出的表单处理两大门派:受控组件与非受控组件。
一、 useRef:不仅仅是 DOM 的钩子
很多从 Vue 转 React 的同学(包括曾经的我),第一眼看到 useRef,都会下意识觉得:“哦,这就是用来拿 document.getElementById 的。”
没错,但只对了一半。
1. 它是一个“静音”的容器
useState 和 useRef 都是存储数据的容器,但性格完全不同:
useState(高调) :它是响应式的。你改了它,它立马通知 React:“嘿!我变了,快重新渲染组件!”useRef(低调) :它不是响应式的。它是可变对象的存储器。你改了它的.current属性,它不仅不会触发重新渲染,而且这个值在组件的多次渲染之间是持久化的。
简单总结:
如果你需要一个变量,在组件更新时不丢失,且修改它时不触发视图更新,请用
useRef。
2. 实战:那个总是“捉摸不透”的定时器
我们在写定时器或倒计时时,经常遇到闭包陷阱或者变量重置的问题。看下面这个经典的例子:
JavaScript
import { useEffect, useRef, useState } from 'react';
export default function App() {
// ❌ 错误示范:如果用 let intervalId = null;
// 每次组件 re-render(比如 count 变化时),intervalId 都会被重置为 null,
// 导致 stop() 函数里根本找不到当初那个 id,也就停不下来。
// ✅ 正确姿势:使用 useRef 创建一个“持久化”的引用
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
function start() {
// 防止重复点击导致开启多个定时器
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
console.log('tick~~~~');
}, 1000);
console.log('Timer Started:', intervalRef.current);
}
function stop() {
// 即使组件因为 count 更新渲染了 100 次,intervalRef.current 依然是同一个值
clearInterval(intervalRef.current);
intervalRef.current = null;
console.log('Timer Stopped');
}
// 只是为了演示 ref 在渲染过程中的稳定性
useEffect(() => {
console.log('Current Interval ID:', intervalRef.current);
}, [count]);
return (
<div className="card">
<h3>Ref vs Variable</h3>
<div style={{ gap: '10px', display: 'flex' }}>
<button onClick={start}>开始 Ticking</button>
<button onClick={stop}>停止</button>
<button onClick={() => setCount(count + 1)}>
Count ++ (当前: {count})
</button>
</div>
</div>
);
}
在这个场景下,useRef 就像一个**“默默奉献”**的后台管家,它记住了定时器的 ID,却从不因为自己的变动去干扰 UI 的渲染。
二、 表单江湖:受控 vs 非受控
理解了 useRef,我们就能更好地理解 React 处理表单的两种模式。
1. 非受控组件 (Uncontrolled Components)
核心心法:useRef + ref
这更像我们以前写 jQuery 或者原生 JS 的感觉。表单数据由 DOM 元素自己接管,React 只在需要的时候(比如提交时)去“读取”一下值。
适用场景:
- 表单非常简单,不需要实时验证。
- 文件上传(
<input type="file" />必须是非受控的)。 - 性能敏感场景(不想每敲一个字都导致组件重绘)。
JavaScript
import { useRef } from "react";
// 就像一个“黑盒”,我们不关心输入过程,只关心结果
export default function CommentBox() {
const textareaRef = useRef(null);
const handleSubmit = () => {
// 直接操作 DOM 获取值
const comment = textareaRef.current.value;
if (!comment.trim()) return alert('请输入评论内容');
console.log('提交评论:', comment);
// 甚至可以手动清空
textareaRef.current.value = '';
}
return (
<div className="comment-box">
<h4>非受控组件示例</h4>
<textarea ref={textareaRef} placeholder="想说点什么..." rows={4} />
<br/>
<button onClick={handleSubmit}>发布评论</button>
</div>
)
}
2. 受控组件 (Controlled Components)
核心心法:useState + value + onChange
这是 React 官方推荐的“正统”写法。数据驱动页面,State 是唯一的数据源(Single Source of Truth)。输入框的 value 被 state 锁死,用户的输入通过 onChange 更新 state,state 更新再重新渲染 input 的 value。
适用场景:
- 需要实时表单校验(比如密码强度检测)。
- 条件禁用提交按钮(比如必填项未填,按钮置灰)。
- 输入格式化(比如输入电话号码自动加空格)。
JavaScript
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);
}
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '300px' }}>
<h4>受控组件示例</h4>
<input
type="text"
placeholder="请输入用户名"
name="username"
// ⭐️ Value 被状态控制
value={form.username}
// ⭐️ 状态随输入改变
onChange={handleChange}
/>
<input
type="password"
placeholder="请输入密码"
name="password"
value={form.password}
onChange={handleChange}
/>
{/* 受控的好处:可以实时根据状态控制 UI */}
<button type="submit" disabled={!form.username || !form.password}>
{(!form.username || !form.password) ? '请填写完整' : '注册'}
</button>
</form>
)
}
三、 总结:该怎么选?
| 特性 | 受控组件 (Controlled) | 非受控组件 (Uncontrolled) |
|---|---|---|
| 数据源 | React State (useState) | DOM 自身 (useRef) |
| 实时性 | 高(每输入一个字都在渲染) | 低(提交时才读取) |
| 代码量 | 较多 (需要写 onChange) | 较少 |
| 数据流 | 单向数据流,逻辑清晰 | 双向/命令式,简单直接 |
| 最佳用途 | 复杂表单、实时校验、联动逻辑 | 简单取值、文件上传、第三方库集成 |
一句话建议:
如果你的表单交互复杂,或者你需要完全掌控数据,请受控;如果你只是想做一个简单的搜索框或者为了性能优化,偶尔非受控一下(用 useRef)也是极好的。
React 的世界里没有绝对的对错,只有适不适合。只要理解了 useRef 那个“可变但静音”的特性,你就能在组件渲染的洪流中,稳稳地拿捏住每一个状态。