front-end-interview-handbook/packages/react-interview-playbook/contents/react-forms/zh-CN.mdx

794 lines
22 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: React 访谈中的表单
description: 构建交互式 React 表单的指南,涵盖受控与非受控组件、各种输入类型、复杂的状态管理以及强大的错误处理和验证策略
---
React 中的表单是构建交互式应用程序的关键部分,允许用户输入和提交数据。在 React 中,使用状态控制表单元素很常见,这使得它们既动态又可预测。
## 受控与非受控表单组件
在 React 中,表单输入可以通过两种方式管理:**受控**和**非受控**。主要区别在于表单数据的处理方式。
### 受控表单组件
在**受控表单组件**中React 管理表单元素的状态。输入的 value 存储在状态变量中,并通过 `onChange` 处理程序进行更新。
```jsx
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 状态。
```jsx
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 状态中。要访问提交时的表单数据,我们可以:
1. **使用 `FormData`**:直接从 `event.target` 访问表单元素,从表单创建 `FormData` 实例,并使用 `formData.get('name')` 检索值(对应于 `<input>` 上的 `name` 属性)
2. **使用 refs**:使用 `useRef()` 直接引用 `<input>` 字段。可以通过 `nameRef.current.value` 访问输入值
### 何时使用哪个?
| 特性 | 受控表单 | 非受控表单 |
| --- | --- | --- |
| **存储状态的位置** | React 状态 (`useState`) | 原生 DOM |
| **性能** | 需要在更新时重新渲染,可能导致大型表单出现问题 | 对于简单用例更有效率 |
| **验证** | 易于实现 | 需要手动验证 |
| **表单重置** | 容易 (`setState("")`) | 需要 `ref.current.value = ""` 或触发 `'reset'` 事件 |
| **用例** | 动态表单、验证、实时更新 | 简单表单、文件上传或集成非 React 代码 |
* 当您需要动态验证、操作或跟踪用户输入(例如,根据先前的响应切换某些表单字段的可见性)或嵌套表单状态时,请使用**受控表单组件**
* 当使用大型表单、与非 React 代码集成或优化性能时,请使用**非受控表单组件**
在面试中,考虑到表单通常没有很多字段,受控组件和非受控方法都是可行的。就我个人而言,我们倾向于使用受控组件,因为您可能会被要求重置值、添加输入时验证等,并且它更容易实现。
## 处理不同的输入类型
以下是使用受控方法的各种输入类型的代码示例,这意味着它们的值存储在状态中,并通过 `onChange` 处理程序进行更新。
### 文本输入
受控文本输入会在用户输入时更新其在状态中的值。
```jsx
import { useState } from 'react';
function TextInputExample() {
const [text, setText] = useState('');
return (
<div>
<label htmlFor="name-input">Name</label>
<input
id="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`** 进行颜色选择
### 复选框输入
复选框是一个**布尔值**(选中或未选中)。
```jsx
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>
);
}
```
* `checked` 属性绑定到 `isChecked` 状态
* `onChange` 事件使用 `setIsChecked(event.target.checked)` 更新状态
* 这允许 React 跟踪复选框的选中状态
### 单选组
当从**多个选项中选择一个选项**时,使用单选按钮。
```jsx
import { useState } from 'react';
function RadioGroupExample() {
const [gender, setGender] = useState('');
return (
<div>
<div>
<input
id="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>
<input
id="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` 用于多行文本输入。
```jsx
import { useState } from 'react';
function TextAreaExample() {
const [bio, setBio] = useState('');
return (
<div>
<label htmlFor="bio-input">Bio:</label>
<textarea
id="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>` 下拉菜单允许用户选择一个选项。
```jsx
import { useState } from 'react';
function SelectExample() {
const [fruit, setFruit] = useState('apple');
return (
<div>
<label htmlFor="favorite-fruit">Favorite fruit</label>
<select
id="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` 提供了一种结构化的方式来管理状态更新。
```jsx
import { useReducer } from 'react';
// Define reducer function
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return initialState;
default:
return state;
}
}
// Initial form state
const 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>
<input
type="text"
name="name"
value={state.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={state.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="number"
name="age"
value={state.age}
onChange={handleChange}
placeholder="Age"
/>
<button type="button" onClick={handleReset}>
Reset
</button>
</form>
);
}
```
* 保持状态更新可预测且集中
* 适用于具有条件逻辑和依赖关系的表单
* 与 `useState` 相比,可防止不必要的重新渲染
### 处理动态字段
当表单允许用户**动态添加或删除字段**(例如,多个电子邮件输入)时,状态应该是一个数组
```jsx
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}>
<input
type="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](https://react-hook-form.com/) 和 [Formik](https://formik.org/)(未维护)这样的库简化了复杂的表单状态处理。
```jsx
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`
### 处理嵌套表单结构
有时,表单具有嵌套对象(例如,包含街道、城市和邮政编码的地址)。
```jsx
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>
<input
type="text"
name="name"
placeholder="Name"
value={form.name}
onChange={(event) => setForm({ ...form, name: event.target.value })}
/>
<input
type="text"
name="street"
placeholder="Street"
value={form.address.street}
onChange={handleChange}
/>
<input
type="text"
name="city"
placeholder="City"
value={form.address.city}
onChange={handleChange}
/>
<input
type="text"
name="zip"
placeholder="ZIP Code"
value={form.address.zip}
onChange={handleChange}
/>
</form>
);
}
```
* 帮助在一个对象中**干净地**管理相关数据
* 适用于**地址、个人资料或嵌套表单部分**
### 复杂表单状态的最佳实践
* 使用 `useReducer` 进行结构化状态更新
* 使用动态字段进行灵活的输入处理
* 利用 React Hook Form 或 Formik 等表单库来简化验证和状态管理
* 处理分组表单数据时使用嵌套对象
## 错误处理和验证策略
处理错误和验证用户输入对于在 React 表单中创建流畅的用户体验至关重要。验证可确保用户在提交前输入正确的数据,从而防止无效条目并减少后端错误。以下是有效处理验证和错误的各种策略。
### 使用 `useState` 进行基本的客户端验证
对于简单的表单,您可以使用**基于状态的验证**来在提交前检查用户输入。
```jsx
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 passes
alert(`Submitted: ${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>
);
}
```
* 使用本地状态来跟踪错误
* 在验证失败时显示错误消息
* 简单但足以满足基本的验证需求
### 使用正则表达式验证输入字段
对于**结构化输入字段**(如电子邮件、电话号码或密码),**基于正则表达式的验证**很有用。
```jsx
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>
);
}
```
* 使用正则表达式检查电子邮件是否遵循有效格式
* 为用户提供实时反馈
### 浏览器内置的 HTML5 验证
现代浏览器为某些输入类型提供了内置验证。
```jsx
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* 提供了优化的验证,并最大限度地减少了重新渲染。
```jsx
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>
);
}
```
* **使用 refs 而不是 state**,最大限度地减少重新渲染
* **更好的性能**,适用于大型表单
* **内置错误处理**,使用 `formState.errors`
### 处理服务器端验证
即使使用客户端验证,后端验证也是必要的。您需要知道如何显示来自 API 响应的错误消息。
```jsx
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:
<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>
);
}
```
* 使用 `fetch()` 在服务器端验证电子邮件
* 如果验证失败,则显示 API 错误消息
* 防止安全风险(例如,黑客绕过客户端验证)
## 面试的最佳实践
* 对于简单的表单,受控输入和非受控输入都是可行的
* 包装在 `<form>` 中并利用浏览器表单提交事件
* 尽可能使用 HTML5 验证,以减少重新渲染,从而获得更好的性能
* 显示清晰的错误消息,指导用户如何修复其输入
* 仅仅客户端验证是不够的。还需要服务器端验证以防止安全漏洞
* 如果允许使用第三方库例如在家庭作业中React Hook Form 是一个很好的补充,它提供了许多有用的功能,可以帮助您同时实现更好的性能
## 面试中您需要知道的内容
* **非受控和受控表单**:有什么区别,如何使用,以及何时使用
* **表单控件**:能够构建使用各种输入类型的表单
* **复杂表单**:如何构建复杂表单和最佳实践
* **错误处理和验证**:如何使用原生浏览器方法和 React 方法验证表单并显示错误
## 练习题
**编码**
* [生成表格](/questions/user-interface/generate-table/react?framework=react\&tab=coding)
* [联系表单](/questions/user-interface/contact-form/react?framework=react\&tab=coding)
* [抵押贷款计算器](/questions/user-interface/mortgage-calculator/react?framework=react\&tab=coding)
* [温度转换器](/questions/user-interface/temperature-converter/react?framework=react\&tab=coding)
* [航班预订器](/questions/user-interface/flight-booker/react?framework=react\&tab=coding)
* [身份验证代码输入](/questions/user-interface/auth-code-input/react?framework=react\&tab=coding)