React 表单面试中受控组件与非受控组件怎么选?
【免费下载链接】front-end-interview-handbookFront End interview preparation materials for busy engineers (updated for 2026)项目地址: https://gitcode.com/GitHub_Trending/fr/front-end-interview-handbook
在 front-end-interview-handbook 的 React 面试手册中,表单是反复出现的一类编码题(Contact Form、Auth Code Input 等都涉及)。动手写表单之前,第一个决策就是输入值放在哪里:React state(受控组件)还是原生 DOM(非受控组件)。这个选择决定了后续写onChange还是写ref,也决定了面试官追问"实时校验""表单重置"时你能多快兑现。下面的实现和判断依据全部来自手册中 Forms in React Interviews 与 Signup Form Example 两篇文档。
受控与非受控:两种写法的完整实现
受控组件:值存在 React state 中
受控组件中,React 管理表单元素的状态:输入值存储在 state 变量中,通过onChange处理程序更新,值始终由 React 控制。
import { useState } from 'react'; function ControlledForm() { const [name, setName] = useState(''); function handleChange(event) { setName(event.target.value); } function handleSubmit(event) { event.preventDefault(); alert(`Submitted Name: ${name}`); } return ( <form onSubmit={handleSubmit}> <label> Name: <input type="text" value={name} onChange={handleChange} /> </label> <button type="submit">Submit</button> </form> ); }用户每敲一个字符,onChange就用setName(event.target.value)更新 state;提交时直接从 state 读取当前值。
非受控组件:值留在 DOM 中
非受控组件中,输入值不由 React state 管理,而是由 DOM 本身持有。提交时通过两种方式取值:
FormData:从event.target拿到表单元素,formData.get('name')的值对应<input>上的name属性;- refs:用
useRef()直接引用<input>,通过nameRef.current.value读取。
import { useRef } from 'react'; function UncontrolledForm() { const nameRef = useRef(); function handleSubmit(event) { event.preventDefault(); // Access form values using `FormData` const formData = new FormData(event.target); console.log('Name:', formData.get('name')); // Alternatively, access the <input> via a ref console.log('Name:', nameRef.current.value); } return ( <form onSubmit={handleSubmit}> <label> Name: <input type="text" name="name" ref={nameRef} /> </label> <button type="submit">Submit</button> </form> ); }选择依据:对照表与面试中的默认倾向
手册给出的两种表单特性对照:
| 特性 | 受控表单 | 非受控表单 |
|---|---|---|
| 状态存储位置 | React 状态(useState) | 原生 DOM |
| 性能 | 更新时需要重新渲染,大型表单可能出现问题 | 简单用例下性能更好 |
| 校验 | 易于实现 | 需要手动校验 |
| 表单重置 | 容易(setState("")) | 需要ref.current.value = ""或触发'reset'事件 |
| 用例 | 动态表单、校验、实时更新 | 简单表单、文件上传、集成非 React 代码 |
对应的选择规则:
- 需要动态校验、操作或跟踪用户输入(例如根据先前回答切换某些字段的可见性),或涉及嵌套表单状态时,用受控组件;
- 处理大型表单、与非 React 代码集成、或以性能为优先时,用非受控组件。
手册对面试的明确建议:面试中的表单通常字段不多,两种方式都可行,但作者倾向默认选受控组件,除非表单极其简单。原因是面试官经常追加 follow-up,比如实时校验(live validation)、条件字段(conditional fields)、程序化重置(programmatic reset)——这些用受控 state 实现很直接,用 refs 和FormData则要额外搭管道。
受控写法的细节:checkbox 必须绑checked
文本类输入用value+event.target.value,而复选框是布尔值,这是文档标注的"常见面试失误点":checkbox 绑checked而非value,读取event.target.checked而非event.target.value。
import { useState } from 'react'; function CheckboxExample() { const [isChecked, setIsChecked] = useState(false); return ( <div> <input id="checkbox-input" type="checkbox" checked={isChecked} onChange={(event) => setIsChecked(event.target.checked)} /> <label htmlFor="checkbox-input">Agree to terms and conditions</label> <p>Checkbox is {isChecked ? 'checked' : 'unchecked'}</p> </div> ); }单选组同理:用同一个name属性(如name="gender")保证互斥,每个选项用checked={gender === 'male'}这类比较决定选中态,onChange更新为event.target.value。
各类受控输入的完整对照(来自手册汇总表):
| 输入类型 | 关键元素 | 关键值属性 | 状态更新 |
|---|---|---|---|
| 文本输入 | <input> | value | setState(event.target.value) |
| 复选框输入 | <input> | checked | setState(event.target.checked) |
| 单选组 | <input> | checked | setState(event.target.value) |
| 文本区域 | <textarea> | value | setState(event.target.value) |
| 选择下拉菜单 | <select> | value | setState(event.target.value) |
注意<textarea>与 HTML 不同:React 用value属性控制,而不是在标签内写文本。
文本输入还会考type属性的选用,手册给出的对照表:
type | 用途 | 内置校验 |
|---|---|---|
text | 常规文本输入 | 无 |
number | 数字文本输入 | 有,仅允许数字,可配合min/max |
email | 电子邮件地址 | 有,必须包含@ |
password | 安全密码输入 | 无,但输入会被掩码 |
search | 带清除按钮的搜索字段 | 无 |
tel | 电话号码 | 无,可配合pattern |
url | URL | 有,必须以http://或https://开头 |
datetime-local | 日期和时间选择 | 有 |
color | 颜色选择器 | 有 |
非受控写法的配套要求
非受控表单的值走 DOM,因此 Signup Form 文档中的几条 HTML 要求直接决定它能不能工作:
name属性是表单数据的键:没有name的<input>不会出现在提交数据中,formData.get('name')也取不到对应值;name与value配对后才会进入 HTTP 请求;- 每个输入配一个
<label>:通过htmlFor+id关联(或把<input>包在<label>里)。placeholder 不是 label——用户输入后会消失,屏幕阅读器也可能跳过; - 控件包在
<form>内:这样才能获得浏览器的回车提交行为,并在onSubmit中拿到包含所有字段数据的event.target。
面试追问怎么兑现
输入时实时校验
受控组件做实时校验最省事:把错误信息也放进 state,提交(或每次输入)时检查,失败时渲染错误文案。手册示例(在其基础上补了文档省略的useStateimport):
import { useState } from 'react'; function EmailValidationForm() { const [email, setEmail] = useState(''); const [error, setError] = useState(''); const validateEmail = (email) => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }; function handleSubmit(event) { event.preventDefault(); if (!validateEmail(email)) { setError('Please enter a valid email address'); return; } setError(''); alert(`Valid Email: ${email}`); } return ( <form onSubmit={handleSubmit}> <label> Email: <input type="email" value={email} onChange={(event) => setEmail(event.target.value)} /> </label> {error && <p style={{ color: 'red' }}>{error}</p>} <button type="submit">Submit</button> </form> ); }表单重置
对照表里的重置行就是两种实现的分叉点:
- 受控:直接
setState("")(多字段可用useReducer的RESETaction 原子化重置全部字段); - 非受控:写
ref.current.value = "",或触发'reset'事件。
这正是"默认受控"建议的由来:重置是高频 follow-up,受控下是一行 state 更新,非受控下要逐个处理 ref。
HTML5 原生校验:两种实现都可以叠加
required、minLength、pattern等属性由浏览器在提交前校验,无需 JavaScript,能减少因无效输入引起的重渲染。非受控表单里尤其常用,因为浏览器会替你挡掉第一次校验:
function HTML5ValidationForm() { return ( <form> <label> Email: <input type="email" required /> </label> <br /> <label> Password (Min 6 characters): <input type="password" minLength="6" required /> </label> <br /> <label> Phone (Numbers only): <input type="tel" pattern="[0-9]{10}" required /> </label> <br /> <button type="submit">Submit</button> </form> ); }但手册明确警告:客户端校验只是体验层面的便利,不是安全边界。攻击者可以完全绕过你的 React 表单直接请求接口,任何你在意的约束(必填、长度、格式、唯一性)都必须服务端重查。手册给出的服务端校验示例是向/api/validate-email发fetch()请求,response.ok为假时抛data.message,在catch中setError(err.message)并渲染到表单下方。这条"服务端必须再校验一次"的结论在 Signup Form 文档中同样出现,两篇文档口径一致。
可选分支:允许第三方库时使用 React Hook Form
文档说明,若题目允许第三方库(如 take-home 作业),React Hook Form 是手册推荐的补充,它用 refs 而非 state 来减少重渲染,并内置校验支持。动态字段用useFieldArray处理。Formik 被文档标注为 unmaintained,不建议作为面试选型。带校验的注册写法:
import { useForm } from 'react-hook-form'; function HookFormValidation() { const { register, handleSubmit, formState: { errors }, } = useForm(); const onSubmit = (data) => alert(JSON.stringify(data)); return ( <form onSubmit={handleSubmit(onSubmit)}> <label> Email: <input type="email" {...register('email', { required: 'Email is required' })} /> </label> {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>} <label> Password: <input type="password" {...register('password', { required: 'Password is required', minLength: { value: 6, message: 'Password must be at least 6 characters', }, })} /> </label> {errors.password && ( <p style={{ color: 'red' }}>{errors.password.message}</p> )} <button type="submit">Submit</button> </form> ); }错误信息统一从formState.errors读取并渲染。
提交后如何核对结果
手册示例自带的核对方式就是提交处理程序里的输出,可直接用来验证两条实现路径是否等价:
- 受控表单:提交后
alert(Submitted Name: ${name})弹出 state 中的值(文档示例输出,值取决于你输入的内容); - 非受控表单:
console.log会先后打印formData.get('name')和nameRef.current.value,两者应一致且等于你输入的值。
如果非受控表单里formData.get('name')拿到的是null,先检查<input>是否漏了name属性——没有name的输入项不会进入FormData,也不会进入提交数据,这是 Signup Form 文档明确指出的行为。
继续练习
手册在 Forms 章节末尾列出的编码练习题可直接用来验证本文的选择路径:Contact Form(联系表单)、Auth Code Input(身份验证代码输入)、Generate Table(生成表格)、Mortgage Calculator(抵押贷款计算器)、Temperature Converter(温度转换器)、Flight Booker(航班预订器)。按"字段少、可能需要实时校验"的表单题,默认走受控;只有题目强调大表单性能或文件上传时才考虑非受控。
延伸阅读:Forms in React Interviews、Signup Form Example in React。
【免费下载链接】front-end-interview-handbookFront End interview preparation materials for busy engineers (updated for 2026)项目地址: https://gitcode.com/GitHub_Trending/fr/front-end-interview-handbook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考