React 受控组件
1. 引入
在 HTML 中,像 <input>, <textarea>, 和 <select> 这样的表单元素通常会自己维护其内部状态,并根据用户的输入自动更新:
<input type="text" name="username" />
当我们在这个输入框里打字时,是 DOM 节点本身在保存和显示这个值。
但是,React 组件无法直接知道或控制这个值。这就导致了状态的分裂:一部分状态在 React 组件里,另一部分则“隐藏”在 DOM 中。这种分裂会使表单的验证、重置和条件逻辑变得复杂和不可预测。
在 React 中,将组件的状态(State)作为“单一数据源”是管理和组织代码的核心思想。受控组件(Controlled Components)正是这一思想在处理表单时的体现。受控组件通过将表单数据完全交给 React 组件来管理,解决了这个问题。
2. 受控组件
核心思想
受控组件的核心思想非常简单:让 React 的 state 成为表单元素值的唯一数据源。一个受控组件的工作流程如下:
- 状态驱动视图:将 React 组件
state中的一个变量绑定到表单元素的value属性上。 - 视图更新状态:为表单元素提供一个
onChange事件处理函数。当用户输入时,这个函数会触发,并使用用户的输入来更新 React 的state。 - 重新渲染:
state的更新会触发组件的重新渲染,表单元素会显示更新后的state值。
通过这种方式,组件的状态和表单的值始终保持同步,React 组件完全“控制”了表单。这种模式带来了如下的好处:
- 单一数据源:组件的
state包含了整个表单的当前状态,使得调试和状态管理变得清晰、可预测。 - 即时验证与反馈:可以在
onChange处理函数中对用户的每次输入进行实时验证,并立即通过 UI 反馈给用户(例如,显示错误信息或禁用提交按钮)。 - 强制格式化:可以轻松实现输入格式化,如信用卡号自动添加空格、输入内容自动转为大写等。
- 条件逻辑:可以根据一个输入的值,动态地改变另一个输入的状态(例如,选择“其他”选项时,显示一个文本框)。
实现一个受控输入框
下面是一个典型的受控组件实现:
import React, { useState } from "react";
function NameForm() {
// 1. 创建一个 state 变量来存储输入框的值
const [name, setName] = useState("");
// 2. 创建一个 onChange 事件处理函数
const handleChange = (event) => {
// event.target.value 包含了输入框的当前文本
setName(event.target.value.toUpperCase()); // 可以在这里对输入进行格式化
};
const handleSubmit = (event) => {
event.preventDefault();
alert("A name was submitted: " + name);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
{/* 3. 将 state 绑定到 value,并将处理函数绑定到 onChange */}
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
在这个例子中,name 状态是输入框值的“唯一真相”。无论用户输入什么,输入框显示的内容都由 name 决定。handleChange 函数作为中间人,确保了用户的操作能够正确地更新这个“唯一真相”。
3. 常见的受控组件
受控组件模式可以应用于所有表单元素。
文本域
在 HTML 中,<textarea> 的值由其子节点决定。但在 React 中,它和 input 一样,也使用 value 属性。
const [story, setStory] = useState("Once upon a time...");
const handleStoryChange = (event) => {
setStory(event.target.value);
};
<textarea value={story} onChange={handleStoryChange} />;
下拉框
在 React 中,我们不再使用 selected 属性来标记 <option>,而是在根 <select> 标签上使用 value 属性来指定当前选中的值。
const [flavor, setFlavor] = useState("coconut");
const handleFlavorChange = (event) => {
setFlavor(event.target.value);
};
<select value={flavor} onChange={handleFlavorChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>;
这种方式使得 <select> 标签与其他受控组件的处理方式完全一致,极大地简化了心智模型。
4. 处理多个输入
当一个表单中有多个输入框时,为每个输入框都创建一个单独的 onChange 处理函数会很繁琐。我们可以通过给每个元素添加 name 属性,让一个处理函数动态地管理多个输入。
function Reservation() {
const [formState, setFormState] = useState({
isGoing: true,
numberOfGuests: 2,
});
const handleInputChange = (event) => {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
// 使用计算属性名 [name] 来动态更新对应的 state
setFormState({
...formState,
[name]: value,
});
};
return (
<form>
<label>
Is going:
<input
name="isGoing"
type="checkbox"
checked={formState.isGoing}
onChange={handleInputChange}
/>
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests"
type="number"
value={formState.numberOfGuests}
onChange={handleInputChange}
/>
</label>
</form>
);
}
5. 对比:非受控组件
与受控组件相对的是非受控组件(Uncontrolled Components)。在非受控组件中,表单数据由 DOM 节点本身处理。我们需要使用 ref 来从 DOM 中“拉取”表单的值,通常是在表单提交时。
function UncontrolledForm() {
const inputRef = React.useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
alert("A name was submitted: " + inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" ref={inputRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
虽然非受控组件在代码上可能更简单,尤其对于非常简单的表单,但它失去了受控组件带来的诸多优势。