公司动态

React 表单处理:受控 vs 非受控,一个登录框教会你如何选型

📅 2026/8/12 17:04:48
React 表单处理:受控 vs 非受控,一个登录框教会你如何选型
大家好在日常开发中表单永远是最让人“爱恨交加”的模块——尤其是当你面对 React 时总会被一个灵魂拷问拦住这个输入框到底该做成受控还是非受控今天我们不谈虚的直接从一个评论框和登录表单入手把这两种模式掰开揉碎看完就能在项目里落地。一、受控组件React 才是唯一真相来源先看一个最简单的受控输入框import { useState } from react; function ControlledInput() { const [value, setValue] useState(); return ( input typetext value{value} onChange{e setValue(e.target.value)} / ); }你会发现这里的value并没有“自由”地待在 DOM 里而是被 React 的state牢牢握住。每次敲键盘onChange都会把最新的值同步回state然后视图重新渲染。说白了就是表单数据不是从 DOM 里取的而是从 state 里取的。React 成了“唯一数据源”。这种模式就叫受控组件Controlled Component。它的核心特征是表单元素的value/checked等属性由 React state 控制。用户输入 → 触发事件 →setState→ 视图更新形成闭环。优势很明显由于数据一直停留在 state 中你可以在任何时刻对输入做「实时处理」——校验、格式化、联动、禁用按钮……全都水到渠成。二、非受控组件像 jQuery 那样直取 DOM受控组件虽好但有时我们只想要一个“随用随取”的输入框不想为了一个简单值去写useState和onChange。这时非受控组件就派上用场了import { useRef } from react; function UncontrolledInput() { const inputRef useRef(null); function handleClick() { console.log(inputRef.current.value); } return ( input typetext ref{inputRef} / button onClick{handleClick}获取输入值/button / ); }这里没有value和onChange输入的值老老实实待在 DOM 里。React 完全不关心它变成什么样只是通过ref这个“逃生舱”在需要时手动读取。再举一个真实场景评论框。通常我们不需要实时监控用户在 textarea 里打了什么字只想在点击提交时拿到内容function CommentBox() { const textareaRef useRef(null); function handleSubmit() { const comment textareaRef.current.value; if (!comment) return; console.log(comment); // 发送请求后清空 textareaRef.current.value ; } return ( div textarea placeholder请输入评论…… ref{textareaRef}/textarea button onClick{handleSubmit}提交评论/button /div ); }这种模式的本质是表单数据由 DOM 自己保管React 只在必要时通过 ref 去“索取”。这和 Vue 中的ref获取元素值很像甚至让人想起 jQuery 时代的$(#xxx).val()。三、一张表看懂区别受控组件非受控组件数据保存位置React stateDOM 节点更新方式onChangesetStateref读取数据流单向state → view传统 DOM 事件实时校验天然支持需要额外处理一般提交时校验适用场景复杂表单、实时交互、联动简单输入、一次性取值代码量初期偏多更少四、实战一个注册表单的进化之路理论讲再多不如上复杂点的真实代码。下面是一个受控方式的注册表单它同时管理多个字段function RegisterForm() { 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 ( input nameusername typetext value{form.username} onChange{handleChange} placeholder请输入用户名 / input namepassword typepassword value{form.password} onChange{handleChange} placeholder请输入密码 / button onClick{handleSubmit}提交/button / ); }这里用到了两个关键技巧通过name属性区分字段在handleChange中用计算属性名[e.target.name]动态更新对应字段。必须使用...form展开旧状态确保 React 的不可变更新原则。这种写法基本是中小型表单的“万金油”模式配合 TypeScript 体验更佳。接下来看一个更完整的登录表单它加入了实时校验和按钮禁用function LoginForm() { const [form, setForm] useState({ username: , password: }); const [errors, setErrors] 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 6) msg 密码长度不能小于6位; } setErrors(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 !errors.username !errors.password; const handleSubmit (e) { e.preventDefault(); if (!isValid) return; console.log(form, 提交登录); }; return ( form onSubmit{handleSubmit} input nameusername value{form.username} onChange{handleChange} / {errors.username span{errors.username}/span} input namepassword typepassword value{form.password} onChange{handleChange} / {errors.password span{errors.password}/span} button typesubmit disabled{!isValid}登录/button /form ); }注意这里的设计思路实时校验每个字段改变时立刻触发validate将错误信息存入errors状态。按钮状态通过isValid聚合判断决定按钮是否可点击。所有状态都在 React 掌控之中没有任何 DOM 的直接操作。这种复杂度的交互如果用非受控方式实现代码量并不会少反而会因手动操作 DOM、自行管理错误显示而变得杂乱。当你的表单开始需要实时校验、条件渲染、动态输入项时你会发现受控组件的“麻烦”其实是最大的捷径。五、组件多了怎么管index.js统一导出的艺术眼尖的同学一定注意到了在我们的示例代码里App.jsx引入组件时非常简洁import { ControlledInput, UncontrolledInput, CommentBox, RegisterForm, LoginForm } from ./components;而不是写成一长串import ControlledInput from ./components/ControlledInput; import UncontrolledInput from ./components/UncontrolledInput; // ... 还要再写三行这个小技巧就藏在一个不起眼的components/index.js文件里export { default as ControlledInput } from ./ControlledInput; export { default as UncontrolledInput } from ./UncontrolledInput; export { default as CommentBox } from ./CommentBox; export { default as RegisterForm } from ./RegisterForm; export { default as LoginForm } from ./LoginForm;你会发现它做的事情很简单把所有组件从各自的文件中“集中转发”出去。但它带来的好处却非常实在导入路径短且干净只需写from ./componentsNode 会自动找到index.js不用关心内部目录结构。易于维护组件文件改名、增减组件只需修改index.js不会波及App.jsx里的 import 语句。配合 barrel桶文件模式对外暴露出统一接口尤其适合组件库、复杂模块的导出管理。这就像给一堆零散的工具贴了个总目录要用哪个直接从目录里找无需记住每一个抽屉的具体位置。在实际项目中建议在components/、utils/、hooks/等目录下都放置一个index文件让引入变得更优雅。六、选择指南什么时候用哪种没有银弹只有最合适。我个人的判断标准是一眼就能决定用非受控的场景一次性的搜索框、只提交时取值的文本域、文件上传控件input typefile在 React 中只能非受控、需要集成非 React 库的输入。基本都用受控的场景但凡涉及即时反馈如搜索联想、实时校验、字数统计、表单联动省份城市联动、动态表单项、提交前格式化等果断上受控。不要嫌写onChange麻烦后期维护会让你感激当初的自己。还有一点经验之谈同一个表单里可以混合受控与非受控但尽量不要让同一个输入框在两种模式间切换React 会警告。如果某个字段的逻辑特别简单直接useRef取值也未尝不可。七、总结受控组件赋予我们的是对表单数据的绝对控制权非受控组件则是在简单场景下的轻量快捷方式。作为中级开发者你需要的是在合适的时机做出合算的选择而不是被某一种模式绑架。同时别忽略项目中那些小而美的工程化技巧——比如用index.js统一导出组件它能让你在代码量增长时依然保持从容。下次再写表单时不妨先问自己一句“这个输入值我需要在它还没提交前就知道吗” 如果答案是 yes请毫无犹豫地用受控组件并享受 React 单向数据流带来的确定性。