React 访谈中的表单
构建交互式 React 表单的指南,涵盖受控与非受控组件、各种输入类型、复杂的状态管理以及强大的错误处理和验证策略
React 中的表单是构建交互式应用程序的关键部分,允许用户输入和提交数据。在 React 中,使用状态控制表单元素很常见,这使得它们既动态又可预测。
受控与非受控表单组件
在 React 中,表单输入可以通过两种方式管理:受控和非受控。主要区别在于表单数据的处理方式。
受控表单组件
在受控表单组件中,React 管理表单元素的状态。输入的 value 存储在状态变量中,并通过 onChange
处理程序进行更新。
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>);}
在受控组件中,输入值存储在 React 状态 (name
) 中。onChange
处理程序在用户输入时更新状态。这确保了该值始终由 React 控制。
非受控表单组件
在非受控表单组件中,表单元素的值由 DOM 本身管理,而不是 React 状态。
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 refconsole.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 状态中。要访问提交时的表单数据,我们可以:
- 使用
FormData
:直接从event.target
访问表单元素,从表单创建FormData
实例,并使用formData.get('name')
检索值(对应于<input>
上的name
属性) - 使用 refs:使用
useRef()
直接引用<input>
字段。可以通过nameRef.current.value
访问输入值
何时使用哪个?
特性 | 受控表单 | 非受控表单 |
---|---|---|
存储状态的位置 | React 状态 (useState ) | 原生 DOM |
性能 | 需要在更新时重新渲染,可能导致大型表单出现问题 | 对于简单用例更有效率 |
验证 | 易于实现 | 需要手动验证 |
表单重置 | 容易 (setState("") ) | 需要 ref.current.value = "" 或触发 'reset' 事件 |
用例 | 动态表单、验证、实时更新 | 简单表单、文件上传或集成非 React 代码 |
- 当您需要动态验证、操作或跟踪用户输入(例如,根据先前的响应切换某些表单字段的可见性)或嵌套表单状态时,请使用受控表单组件
- 当使用大型表单、与非 React 代码集成或优化性能时,请使用非受控表单组件
在面试中,考虑到表单通常没有很多字段,受控组件和非受控方法都是可行的。就我个人而言,我们倾向于使用受控组件,因为您可能会被要求重置值、添加输入时验证等,并且它更容易实现。
处理不同的输入类型
以下是使用受控方法的各种输入类型的代码示例,这意味着它们的值存储在状态中,并通过 onChange
处理程序进行更新。
文本输入
受控文本输入会在用户输入时更新其在状态中的值。
import { useState } from 'react';function TextInputExample() {const [text, setText] = useState('');return (<div><label htmlFor="name-input">Name</label><inputid="name-input"type="text"value={text}onChange={(event) => setText(event.target.value)}/><p>Entered Text: {text}</p></div>);}
- 输入字段的值由 React 状态 (
text
) 控制 onChange
事件使用setText(event.target.value)
更新状态- 这确保了 React 动态管理输入值
type
属性有许多其他基于文本的值,每个值都有不同的用途:
type | 用途 | 内置验证 |
---|---|---|
text | 常规文本输入 | 否 |
number | 数字文本输入 | 是,仅允许数字。通过 min /max 属性进行其他验证 |
email | 电子邮件地址 | 是,必须包含 @ |
password | 安全密码输入 | 否,但输入被屏蔽 |
search | 带有清除按钮的搜索字段 | 否 |
tel | 电话号码 | 否。通过 pattern 属性进行其他验证 |
url | URL | 是,必须以 http:// 或 https:// 开头 |
datetime-local | 日期和时间选择 | 是 |
color | 颜色选择器 | 是 |
建议:
- 将
text
用于通用输入字段 - 当您只需要数值和数字的内置验证时,使用
number
- 使用
email
、url
和tel
来利用内置验证 - 使用
password
进行安全文本输入 - 使用
search
进行针对搜索优化的字段 - 当需要日期和时间选择时,使用
datetime-local
- 使用
color
进行颜色选择
复选框输入
复选框是一个布尔值(选中或未选中)。
import { useState } from 'react';function CheckboxExample() {const [isChecked, setIsChecked] = useState(false);return (<div><inputid="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>);}
checked
属性绑定到isChecked
状态onChange
事件使用setIsChecked(event.target.checked)
更新状态- 这允许 React 跟踪复选框的选中状态
单选组
当从多个选项中选择一个选项时,使用单选按钮。
import { useState } from 'react';function RadioGroupExample() {const [gender, setGender] = useState('');return (<div><div><inputid="radio-male"type="radio"name="gender"value="male"checked={gender === 'male'}onChange={(event) => setGender(event.target.value)}/><label htmlFor="radio-male">Male</label></div><div><inputid="radio-female"type="radio"name="gender"value="female"checked={gender === 'female'}onChange={(event) => setGender(event.target.value)}/><label htmlFor="radio-female">Female</label></div><p>Selected gender: {gender}</p></div>);}
name="gender"
确保只能选择一个选项checked
属性检查当前值是否与状态匹配 (gender === "male"
或"female"
)onChange
使用选定值更新状态
文本区域
textarea
用于多行文本输入。
import { useState } from 'react';function TextAreaExample() {const [bio, setBio] = useState('');return (<div><label htmlFor="bio-input">Bio:</label><textareaid="bio-input"value={bio}onChange={(event) => setBio(event.target.value)}/><p>Bio Preview: {bio}</p></div>);}
- 与 HTML 不同,React 使用
value
而不是在<textarea>
中设置文本 onChange
使用setBio(event.target.value)
更新状态,确保 React 控制输入
选择下拉菜单
<select>
下拉菜单允许用户选择一个选项。
import { useState } from 'react';function SelectExample() {const [fruit, setFruit] = useState('apple');return (<div><label htmlFor="favorite-fruit">Favorite fruit</label><selectid="favorite-fruit"value={fruit}onChange={(event) => setFruit(event.target.value)}><option value="apple">Apple</option><option value="banana">Banana</option><option value="orange">Orange</option></select><p>Selected fruit: {fruit}</p></div>);}
value
属性绑定到状态 (fruit
)- 当用户选择一个选项时,
onChange
更新状态 - 这确保了下拉菜单的值由 React 控制
总结
输入类型 | 关键元素 | 关键值属性 | 状态更新 |
---|---|---|---|
文本输入 | <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) |
处理复杂的表单状态
在 React 中使用复杂表单时,有效地管理状态对于确保良好的性能和可维护性至关重要。当表单包含多个输入字段、动态字段添加、嵌套结构或高级验证时,表单会变得复杂。以下是一些有效处理复杂表单状态的策略。
使用 useReducer
处理复杂表单
对于具有多个字段和状态依赖关系的表单,useReducer
提供了一种结构化的方式来管理状态更新。
import { useReducer } from 'react';// Define reducer functionfunction formReducer(state, action) {switch (action.type) {case 'UPDATE_FIELD':return { ...state, [action.field]: action.value };case 'RESET':return initialState;default:return state;}}// Initial form stateconst initialState = {name: '',email: '',age: '',};function ComplexFormWithReducer() {const [state, dispatch] = useReducer(formReducer, initialState);function handleChange(event) {dispatch({type: 'UPDATE_FIELD',field: event.target.name,value: event.target.value,});}function handleReset() {return dispatch({ type: 'RESET' });}return (<form><inputtype="text"name="name"value={state.name}onChange={handleChange}placeholder="Name"/><inputtype="email"name="email"value={state.email}onChange={handleChange}placeholder="Email"/><inputtype="number"name="age"value={state.age}onChange={handleChange}placeholder="Age"/><button type="button" onClick={handleReset}>Reset</button></form>);}
- 保持状态更新可预测且集中
- 适用于具有条件逻辑和依赖关系的表单
- 与
useState
相比,可防止不必要的重新渲染
处理动态字段
当表单允许用户动态添加或删除字段(例如,多个电子邮件输入)时,状态应该是一个数组
import { useState } from 'react';function DynamicForm() {const [fields, setFields] = useState([{ id: 1, value: '' }]);function addField() {setFields([...fields, { id: fields.length + 1, value: '' }]);}function removeField(id) {setFields(fields.filter((field) => field.id !== id));}function handleChange(id, event) {const newFields = fields.map((field) =>field.id === id ? { ...field, value: event.target.value } : field,);setFields(newFields);}return (<form>{fields.map((field) => (<div key={field.id}><inputtype="text"value={field.value}onChange={(event) => handleChange(field.id, event)}placeholder="Enter value"/><button type="button" onClick={() => removeField(field.id)}>Remove</button></div>))}<button type="button" onClick={addField}>Add Field</button></form>);}
- 适用于用户可以添加多个条目的表单(例如,多个电子邮件地址)
- 保持 UI 灵活,无需预定义的字段
使用表单库处理复杂表单
除了手动管理所有内容之外,像 React Hook Form 和 Formik(未维护)这样的库简化了复杂的表单状态处理。
import { useForm, useFieldArray } from 'react-hook-form';function FormWithReactHookForm() {const { register, handleSubmit, control } = useForm({defaultValues: { emails: [{ value: '' }] },});const { fields, append, remove } = useFieldArray({ control, name: 'emails' });function onSubmit(data) {console.log(data);}return (<form onSubmit={handleSubmit(onSubmit)}>{fields.map((field, index) => (<div key={field.id}><input {...register(`emails.${index}.value`)} placeholder="Email" /><button type="button" onClick={() => remove(index)}>Remove</button></div>))}<button type="button" onClick={() => append({ value: '' })}>Add Email</button><button type="submit">Submit</button></form>);}
- 减少重新渲染,使用引用而不是状态
- 轻松验证,内置支持 Zod、Yup 和 Joi 等验证库
- 高效处理动态字段,使用
useFieldArray
处理嵌套表单结构
有时,表单具有嵌套对象(例如,包含街道、城市和邮政编码的地址)。
import { useState } from 'react';function NestedForm() {const [form, setForm] = useState({name: '',address: { street: '', city: '', zip: '' },});function handleChange(event) {const { name, value } = event.target;setForm((prev) => ({...prev,address: { ...prev.address, [name]: value },}));}return (<form><inputtype="text"name="name"placeholder="Name"value={form.name}onChange={(event) => setForm({ ...form, name: event.target.value })}/><inputtype="text"name="street"placeholder="Street"value={form.address.street}onChange={handleChange}/><inputtype="text"name="city"placeholder="City"value={form.address.city}onChange={handleChange}/><inputtype="text"name="zip"placeholder="ZIP Code"value={form.address.zip}onChange={handleChange}/></form>);}
- 帮助在一个对象中干净地管理相关数据
- 适用于地址、个人资料或嵌套表单部分
复杂表单状态的最佳实践
- 使用
useReducer
进行结构化状态更新 - 使用动态字段进行灵活的输入处理
- 利用 React Hook Form 或 Formik 等表单库来简化验证和状态管理
- 处理分组表单数据时使用嵌套对象
错误处理和验证策略
处理错误和验证用户输入对于在 React 表单中创建流畅的用户体验至关重要。验证可确保用户在提交前输入正确的数据,从而防止无效条目并减少后端错误。以下是有效处理验证和错误的各种策略。
使用 useState
进行基本的客户端验证
对于简单的表单,您可以使用基于状态的验证来在提交前检查用户输入。
import { useState } from 'react';function BasicValidationForm() {const [email, setEmail] = useState('');const [error, setError] = useState('');function handleSubmit(event) {event.preventDefault();if (!email) {setError('Email is required');return;}setError(''); // Clear error if validation passesalert(`Submitted: ${email}`);}return (<form onSubmit={handleSubmit}><label>Email:<inputtype="email"value={email}onChange={(event) => setEmail(event.target.value)}/></label>{error && <p style={{ color: 'red' }}>{error}</p>}<button type="submit">Submit</button></form>);}
- 使用本地状态来跟踪错误
- 在验证失败时显示错误消息
- 简单但足以满足基本的验证需求
使用正则表达式验证输入字段
对于结构化输入字段(如电子邮件、电话号码或密码),基于正则表达式的验证很有用。
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:<inputtype="email"value={email}onChange={(event) => setEmail(event.target.value)}/></label>{error && <p style={{ color: 'red' }}>{error}</p>}<button type="submit">Submit</button></form>);}
- 使用正则表达式检查电子邮件是否遵循有效格式
- 为用户提供实时反馈
浏览器内置的 HTML5 验证
现代浏览器为某些输入类型提供了内置验证。
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>);}
required
:确保字段已填写minLength
:限制输入长度pattern
:直接在 HTML 中使用正则表达式- 无需 JavaScript 即可工作,提高性能
用于高效验证的 React Hook Form
对于复杂的表单,*React Hook Form 提供了优化的验证,并最大限度地减少了重新渲染。
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:<inputtype="email"{...register('email', { required: 'Email is required' })}/></label>{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}<label>Password:<inputtype="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>);}
- 使用 refs 而不是 state,最大限度地减少重新渲染
- 更好的性能,适用于大型表单
- 内置错误处理,使用
formState.errors
处理服务器端验证
即使使用客户端验证,后端验证也是必要的。您需要知道如何显示来自 API 响应的错误消息。
import { useState } from 'react';function ServerErrorHandlingForm() {const [email, setEmail] = useState('');const [error, setError] = useState('');async function handleSubmit(event) {event.preventDefault();try {setError(null);const response = await fetch('/api/validate-email', {method: 'POST',body: JSON.stringify({ email }),headers: { 'Content-Type': 'application/json' },});const data = await response.json();if (!response.ok) {throw new Error(data.message || 'Something went wrong');}alert('Submission successful!');} catch (err) {setError(err.message);}}return (<form onSubmit={handleSubmit}><label>Email:<inputtype="email"value={email}onChange={(event) => setEmail(event.target.value)}/></label>{error && <p style={{ color: 'red' }}>{error}</p>}<button type="submit">Submit</button></form>);}
- 使用
fetch()
在服务器端验证电子邮件 - 如果验证失败,则显示 API 错误消息
- 防止安全风险(例如,黑客绕过客户端验证)
面试的最佳实践
- 对于简单的表单,受控输入和非受控输入都是可行的
- 包装在
<form>
中并利用浏览器表单提交事件 - 尽可能使用 HTML5 验证,以减少重新渲染,从而获得更好的性能
- 显示清晰的错误消息,指导用户如何修复其输入
- 仅仅客户端验证是不够的。还需要服务器端验证以防止安全漏洞
- 如果允许使用第三方库(例如,在家庭作业中),React Hook Form 是一个很好的补充,它提供了许多有用的功能,可以帮助您同时实现更好的性能
面试中您需要知道的内容
- 非受控和受控表单:有什么区别,如何使用,以及何时使用
- 表单控件:能够构建使用各种输入类型的表单
- 复杂表单:如何构建复杂表单和最佳实践
- 错误处理和验证:如何使用原生浏览器方法和 React 方法验证表单并显示错误
练习题
编码: