面试必问的 React 受控组件,我用 5 个实战场景一次讲透(附完整可运行代码) 为什么value写了却不生效为什么onChange不写就变成只读输入框为什么表单校验要写一堆代码这三个问题是无数前端面试官的「送命题」。如果你正在做表单、写输入框或者准备面试这篇就是为你写的。看完你能解决 3 个问题受控组件和非受控组件到底有什么区别一句话讲清什么时候该用受控什么时候该用非受控不再纠结表单校验、动态取值这些实战需求直接抄代码就能用全程短句、短段落代码完整可复制建议先收藏再慢慢看。一、先说痛点你是不是也这样写过刚学 React 的时候很多人写输入框是这么干的// 错误示范value 写死了输入框永远改不了 const BadInput () { const [value, setValue] useState() return input typetext value{value} / }结果运行一看输入框点都点不动键盘敲半天一个字都输不进去。你以为是浏览器坏了查半天才发现——你没写onChange。这就是受控组件的核心逻辑数据在哪展示就在哪想改数据必须走 setState。二、什么是受控组件3 行代码讲清楚受控组件就是组件的显示值完全由 React 状态控制。用一个useState存值再用onChange更新值形成一个闭环。import { useState } from react; // 受控组件值由 state 控制改值必须走 setState const ControlledInput () { const [value, setValue] useState(); return ( div input typetext value{value} onChange{(e) setValue(e.target.value)} / /div ); }; export default ControlledInput;核心只有 3 个点value来自state数据源唯一onChange里调用setValue更新数据输入框展示的值永远是 state 里最新的值这就是 React 的单向数据流数据流向 UIUI 事件再改数据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 typetext 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 typetext nameusername value{form.username} placeholder请输入用户名 onChange{handleChange} / {error.username span classNameerror{error.username}/span} /div div label密码/label input typetext namepassword value{form.password} placeholder请输入密码 onChange{handleChange} / {error.password span classNameerror{error.password}/span} /div button typesubmit onClick{handleSubmit} disabled{!isValid} 提交 /button /form ); }; export default LoginForm;这段代码值得反复看的 3 个细节name属性 同名 state 字段两个输入框共用一个handleChange靠name区分不用写两遍实时校验onChange里既更新form又调用validate错误信息跟着输入同步刷新提交拦截disabled{!isValid}让不合法的表单根本提交不了踩坑提醒别忘了e.preventDefault()。不写的话点提交页面会刷新你 console 出来的数据瞬间就没了——这个坑踩过的人都懂。六、受控 vs 非受控一张表看懂对比项受控组件非受控组件数据存放位置React stateDOM 节点自身取值方式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一会儿用defaultValueReact 会警告行为也混乱。要么受控到底要么非受控到底。八、到底怎么选给你一个决策清单别死记硬背遇到输入类组件按这个清单走需要实时校验 / 实时提示错误→受控需要根据输入实时联动选类型加载选项→受控需要重置表单 / 初始化清空→受控改 state 就行只想提交时拿一次值→非受控数据量大、频繁输入不想每次 re-render→非受控接入第三方非 React 库如富文本编辑器→非受控ref 直接接管 DOM大方向记住表单优先受控简单取值用非受控。九、总结回到开头那三个「送命题」现在你都会了受控 vs 非受控受控是 React 管数据state非受控是 DOM 自己管数据ref什么时候用谁要实时就受控要终值就非受控表单场景优先受控表单校验怎么写value onChange name 分发 实时校验五步搞定面试官再问你就按这三句话回答稳了。最后源码在这里这篇文章里所有代码我都放在一个完整的 Vite React demo 里了直接npm install npm run dev就能跑。仓库里有5 个组件从最简单的受控/非受控输入框到带校验的登录表单每个都能直接复制进你的项目ControlledInput.jsx受控组件最小示例UncontrolledInput.jsx非受控组件最小示例CommentBox.jsx非受控实战评论框LoginForm.jsx受控实战登录表单 实时校验RegisterForm.jsx注册表单受控写法延伸建议收藏这篇文章等面试或写表单前翻出来看一遍比临时查文档快 10 倍。如果你觉得有用点个赞让我知道下一篇我会拆解为什么受控组件更适合配合表单库比如 antd使用以及封装通用表单组件的实战技巧。