为什么 value 写了却不生效?为什么 onChange 不写就变成只读输入框?为什么表单校验要写一堆代码?这三个问题,是无数前端面试官的「送命题」。
如果你正在做表单、写输入框,或者准备面试,这篇就是为你写的。

看完你能解决 3 个问题:
全程短句、短段落,代码完整可复制,建议先收藏再慢慢看。
一、先说痛点:你是不是也这样写过?
刚学 React 的时候,很多人写输入框是这么干的:
// 错误示范:value 写死了,输入框永远改不了
const BadInput = () => {
const [value, setValue] = useState("")
return <input type="text" value={value} />
}
结果运行一看:输入框点都点不动,键盘敲半天,一个字都输不进去。
你以为是浏览器坏了,查半天才发现——你没写 onChange。
这就是受控组件的核心逻辑:数据在哪,展示就在哪;想改数据,必须走 setState。
二、什么是受控组件?3 行代码讲清楚
受控组件,就是 组件的显示值完全由 React 状态控制。
用一个 useState 存值,再用 onChange 更新值,形成一个闭环。
import { useState } from "react";
// 受控组件:值由 state 控制,改值必须走 setState
const ControlledInput = () => {
const [value, setValue] = useState("");
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
);
};
export default ControlledInput;
核心只有 3 个点:
- value 来自 state,数据源唯一
- onChange 里调用 setValue,更新数据
- 输入框展示的值,永远是 state 里最新的值
这就是 React 的单向数据流:数据流向 UI,UI 事件再改数据,UI 永远不能自己改自己。
三、什么是非受控组件?useRef 才是主角
非受控组件,就是 值存在 DOM 自己身上,React 不去管它。
你想取值?用 useRef 拿到 DOM 节点,直接读 .value。
import { useRef } from "react";
// 非受控组件:值存在 DOM 里,需要时用 ref 取
const UncontrolledInput = () => {
const inputRef = useRef(null);
const handleClick = () => {
console.log(inputRef.current.value); // 点按钮时才取值
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>点击获取值</button>
</div>
);
};
export default UncontrolledInput;
注意看区别:
- 没有 value,只有 ref
- 输入过程中 React 完全不管,DOM 自己记着
- 你想拿值的时候,ref.current.value 一把梭
一句话记住:受控组件是「React 管数据」,非受控组件是「DOM 自己管数据,React 要用时再取」。
四、实战场景 1:评论框 —— 非受控的典型用法
开发博客评论区,用户输入完点提交,你只需要拿到这段文字去调接口。
这种「只在提交那一刻取值」的场景,非受控组件简直不要太爽——不用每次输入都触发 re-render。
import { useRef } from "react";
// 非受控:textarea 的值不经过 React,提交时才读取
const CommentBox = () => {
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current.value;
if (!comment) return;
console.log(comment); // 这里一般是调接口上传
};
return (
<div>
<textarea placeholder="输入评论" ref={textareaRef} />
<button onClick={handleSubmit}>提交评论</button>
</div>
);
};
export default CommentBox;
这个场景为什么选非受控?
- 评论内容不需要实时展示、实时校验
- 不需要因为用户打字就频繁更新组件
- 代码量少,ref 一行搞定
踩坑提醒:ref.current 只有在组件挂载后才有值。在渲染阶段直接读 ref.current,拿到的永远是 null。取值必须放在事件回调或 useEffect 里。
五、实战场景 2:登录表单 —— 受控的典型用法
接下来是面试官最爱考的:实时表单校验。
需求是这样的:
- 用户名、密码不能为空
- 长度不能小于 3
- 有错误就提示,全合法才能点提交
这种「输入一个字符,就要立刻校验、立刻提示」的场景,必须用受控组件。因为只有数据经过 React,你才能每敲一个字符就校验一次。
import { useState } from "react";
const LoginForm = () => {
const [form, setForm] = useState({ username: "", password: "" });
const [error, setError] = useState({});
// 校验规则:返回错误信息,空字符串表示通过
const validate = (name, value) => {
let msg = "";
if (name === "username") {
if (!value) {
msg = "用户名为空";
} else if (value.length < 3) {
msg = "用户名长度不能小于3";
}
}
if (name === "password") {
if (!value) {
msg = "密码为空";
} else if (value.length < 3) {
msg = "密码长度不能小于3";
}
}
setError((prev) => ({
…prev,
[name]: msg,
}));
};
const handleChange = (e) => {
const { name, value } = e.target;
setForm({
…form,
[name]: value,
});
validate(name, value);
};
// 表单合法:用户名密码都有值,且都没有错误
const isValid = form.username && form.password && !error.username && !error.password;
const handleSubmit = (e) => {
e.preventDefault();
if (!isValid) return;
console.log(form); // 这里一般是调登录接口
};
return (
<form>
<h2>登录</h2>
<div>
<label>用户名</label>
<input
type="text"
name="username"
value={form.username}
placeholder="请输入用户名"
onChange={handleChange}
/>
{error.username && <span className="error">{error.username}</span>}
</div>
<div>
<label>密码</label>
<input
type="text"
name="password"
value={form.password}
placeholder="请输入密码"
onChange={handleChange}
/>
{error.password && <span className="error">{error.password}</span>}
</div>
<button type="submit" onClick={handleSubmit} disabled={!isValid}>
提交
</button>
</form>
);
};
export default LoginForm;
这段代码值得反复看的 3 个细节:
踩坑提醒:别忘了 e.preventDefault()。 不写的话,点提交页面会刷新,你 console 出来的数据瞬间就没了——这个坑踩过的人都懂。
六、受控 vs 非受控,一张表看懂
| 数据存放位置 | React state | DOM 节点自身 |
| 取值方式 | state 变量 | ref.current.value |
| 更新方式 | onChange 触发 setState | 浏览器自动,无需干预 |
| 实时校验 / 实时联动 | 支持,简单 | 不支持,很麻烦 |
| 每次输入都 re-render | 会 | 不会 |
| 代码量 | 多一些 | 少一些 |
| 典型场景 | 表单校验、实时搜索、联动 | 评论区、文件上传、一次性表单 |
记住这个判断口诀:
- 要实时拿到值做事情(校验、联动、搜索)→ 用受控
- 只要最终拿到值做事情(提交、上传)→ 用非受控
七、踩坑合集:这 4 个坑,90% 的人都踩过
坑 1:写了 value 不写 onChange,输入框变只读
// 错误:输入框锁死,无法输入
<input value={value} />
// 正确:value 必须配 onChange
<input value={value} onChange={(e) => setValue(e.target.value)} />
坑 2:受控组件想设默认值,用 defaultValue,不是 value
// 错误:用 value 给受控组件设默认值,会锁死输入框
<input value="默认文案" onChange={handleChange} />
// 正确:受控组件想有初始值,直接给 state 初始值
const [value, setValue] = useState("默认文案");
// 正确:非受控组件想有默认值,用 defaultValue
<input defaultValue="默认文案" ref={inputRef} />
坑 3:非受控组件取不到值?多半是时机不对
ref.current 在组件挂载前是 null。一定要在事件回调或 useEffect 里取,不能在渲染函数里直接读。
坑 4:不要一个组件里受控和非受控混用
一个输入框,一会儿用 value,一会儿用 defaultValue,React 会警告,行为也混乱。要么受控到底,要么非受控到底。
八、到底怎么选?给你一个决策清单
别死记硬背,遇到输入类组件,按这个清单走:
- 需要实时校验 / 实时提示错误?→ 受控
- 需要根据输入实时联动(选类型加载选项)?→ 受控
- 需要重置表单 / 初始化清空?→ 受控(改 state 就行)
- 只想提交时拿一次值?→ 非受控
- 数据量大、频繁输入,不想每次 re-render?→ 非受控
- 接入第三方非 React 库(如富文本编辑器)?→ 非受控(ref 直接接管 DOM)
大方向记住:表单优先受控,简单取值用非受控。
九、总结
回到开头那三个「送命题」,现在你都会了:
面试官再问,你就按这三句话回答,稳了。
最后:源码在这里
这篇文章里所有代码,我都放在一个完整的 Vite + React demo 里了,直接 npm install && npm run dev 就能跑。
仓库里有 5 个组件,从最简单的受控/非受控输入框,到带校验的登录表单,每个都能直接复制进你的项目:
- ControlledInput.jsx:受控组件最小示例
- UncontrolledInput.jsx:非受控组件最小示例
- CommentBox.jsx:非受控实战(评论框)
- LoginForm.jsx:受控实战(登录表单 + 实时校验)
- RegisterForm.jsx:注册表单(受控写法延伸)
建议收藏这篇文章,等面试或写表单前翻出来看一遍,比临时查文档快 10 倍。
如果你觉得有用,点个赞让我知道,下一篇我会拆解为什么受控组件更适合配合表单库(比如 antd)使用,以及封装通用表单组件的实战技巧。
网硕互联帮助中心



评论前必须登录!
注册