React 提供了两种标准方法来获取来自<form>
元素的值。第一种方法是实现所谓的受控组件(请参阅我的关于该主题的博文),第二种方法是使用 React 的ref
属性。
受控组件功能强大。受控组件的定义特征是显示的值绑定到组件状态。要更新值,您需要执行附加到表单元素上的onChange
事件处理程序的函数。onChange
函数更新状态属性,这反过来又更新表单元素的值。
(在我们深入之前,如果您只想查看本文的代码示例:请点击此处!)
这是一个受控组件的示例
import React, { Component } from 'react';
class ControlledCompExample extends Component {
constructor() {
super();
this.state = {
fullName: ''
}
}
handleFullNameChange = (e) => {
this.setState({
fullName: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
console.log(this.state.fullName)
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="fullName">Full Name</label>
<input
type="text"
value={this.state.fullName}
onChange={this.handleFullNameChange}
name="fullName" />
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
export default ControlledCompExample;
输入的值是this.state.fullName
(第 7 行和第 26 行)。onChange
函数是handleFullNameChange
(第 10 – 14 行和第 27 行)。
受控组件的主要优点是
- 您可以轻松地验证用户输入。
- 您可以根据受控组件的值动态渲染其他组件。例如,用户从下拉列表中选择的值(例如“狗”或“猫”)可以控制在表单中渲染哪些其他表单组件(例如品种复选框集)。
受控组件的缺点是您必须编写的代码量。您需要一个状态属性作为props
传递给表单元素,并且需要一个函数来更新此属性的值。
对于一个表单元素来说,这不是问题——但是如果您有一个大型的复杂表单(不需要动态渲染或实时验证),那么如果您过度使用受控组件,您会发现自己编写了大量的代码。
获取表单元素值的一种更简单、更省力的方法是使用ref
属性。不同的表单元素和组件组合需要不同的策略,因此本文的其余部分分为以下几个部分。
1. 文本输入、数字输入和选择
文本和数字输入提供了使用ref
的最直接的示例。在输入的ref
属性中,添加一个箭头函数,该函数将输入作为参数。我倾向于将参数命名为与元素本身相同的名称,如第 3 行所示
<input
type="text"
ref={input => this.fullName = input} />
由于它是输入元素本身的别名,因此您可以根据需要命名参数
<input
type="number"
ref={cashMoney => this.amount = cashMoney} />
然后,您获取参数并将其分配给附加到类的this
关键字的属性。输入(即 DOM 节点)现在可作为this.fullName
和this.amount
访问。输入的值可作为this.fullName.value
和this.amount.value
访问。
相同的策略适用于选择元素(即下拉列表)。
<select
ref={select => this.petType = select}
name="petType">
<option value="cat">Cat</option>
<option value="dog">Dog</option>
<option value="ferret">Ferret</option>
</select>
所选的值可作为this.petType.value
访问。
2. 从子组件传递 props 到父组件
使用受控组件,从子组件获取值到父组件非常简单——值已经存在于父组件中!它被传递到子组件。一个onChange
函数也被传递下来,并在用户与 UI 交互时更新值。
您可以在我之前的文章中的受控组件示例中看到这一点。
虽然值在受控组件中已经存在于父组件的状态中,但在使用ref
时并非如此。使用ref
,值驻留在 DOM 节点本身中,并且必须向上传递到父组件。
要将此值从子组件传递到父组件,父组件需要将一个“钩子”(如果可以这样说)传递到子组件。然后,子组件将一个节点附加到“钩子”上,以便父组件可以访问它。
在进一步讨论之前,让我们看一些代码。
import React, { Component } from 'react';
class RefsForm extends Component {
handleSubmit = (e) => {
e.preventDefault();
console.log('first name:', this.firstName.value);
this.firstName.value = 'Got ya!';
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<CustomInput
label={'Name'}
firstName={input => this.firstName = input} />
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
function CustomInput(props) {
return (
<div>
<label>{props.label}:</label>
<input type="text" ref={props.firstName}/>
</div>
);
}
export default RefsForm;
在上面,您看到了一个表单组件RefForm
和一个名为CustomInput
的输入组件。通常,箭头函数位于输入本身,但在这里它作为 prop 传递(参见第 15 行和第 27 行)。由于箭头函数驻留在父组件中,因此this.firstName
的this
位于父组件中。
子组件输入的值被分配给父组件的this.firstName
属性,因此子组件的值可供父组件使用。现在,在父组件中,this.firstName
指的是子组件中的 DOM 节点(即CustomInput
中的输入)。
父组件不仅可以访问输入的 DOM 节点,还可以从父组件内部分配节点的值。这在上面的第 7 行进行了演示。表单提交后,输入的值将设置为“Got ya!”。
这种模式有点令人费解,因此请仔细观察它一段时间,并尝试使用代码,直到理解为止。
您可能最好将单选按钮和复选框设为受控组件,但如果您确实想使用refs
,接下来的两节内容适合您。
3. 单选按钮组
与文本和数字输入元素不同,单选按钮以组的形式出现。组中的每个元素都具有相同的name
属性,如下所示
<form>
<label>
Cat
<input type="radio" value="cat" name="pet" />
</label>
<label>
Dog
<input type="radio" value="dog" name="pet" />
</label>
<label>
Ferret
<input type="radio" value="ferret" name="pet" />
</label>
<input type="submit" value="Submit" />
</form>
“宠物”单选按钮组中有三个选项——“猫”、“狗”和“雪貂”。
由于整个组是我们关注的对象,因此在每个单选按钮输入上设置ref
并不是理想的选择。而且,不幸的是,没有 DOM 节点封装一组单选按钮。
可以通过三个步骤获取单选按钮组的值
- 在
<form>
标签上设置 ref(如下所示的第 20 行)。 - 从表单中提取单选按钮组。在本例中,它是
pet
组(如下所示的第 9 行)。- 这里返回一个节点列表和一个值。在本例中,此节点列表包含三个输入节点和所选的值。
- 请记住,节点列表看起来像数组,但它不是数组,并且缺少数组方法。下一节将详细介绍此主题。
- 使用点表示法获取组的值(如下所示的第 13 行)。
import React, { Component } from 'react';
class RefsForm extends Component {
handleSubmit = (e) => {
e.preventDefault();
// extract the node list from the form
// it looks like an array, but lacks array methods
const { pet } = this.form;
// a set of radios has value property
// checkout out the log for proof
console.log(pet, pet.value);
}
render() {
return (
<div>
<form
onSubmit={this.handleSubmit}
ref={form => this.form = form}>
<label>
Cat
<input type="radio" value="cat" name="pet" />
</label>
<label>
Dog
<input type="radio" value="dog" name="pet" />
</label>
<label>
Ferret
<input type="radio" value="ferret" name="pet" />
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
export default RefsForm;
即使您是从子组件组合表单,此方法也适用。尽管组件中有更多逻辑,但从单选按钮组获取值的技巧保持不变。
import React, { Component } from 'react';
class RefsForm extends Component {
handleSubmit = (e) => {
e.preventDefault();
// extract the node list from the form
// it looks like an array, but lacks array methods
const { pet } = this.form;
// a set of radios has value property
// checkout out the log for proof
console.log(pet, pet.value);
}
render() {
return (
<div>
<form
onSubmit={this.handleSubmit}
ref={form => this.form = form}>
<RadioSet
setName={'pet'}
setOptions={['cat', 'dog', 'ferret']} />
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
function RadioSet(props) {
return (
<div>
{props.setOptions.map(option => {
return (
<label
key={option}
style={{textTransform: 'capitalize'}}>
{option}
<input
type="radio"
value={option}
name={props.setName} />
</label>
)
})}
</div>
);
}
export default RefsForm;
4. 复选框组
与单选按钮组不同,复选框组可以选择多个值。这使得提取这些值比提取单选按钮组的值稍微复杂一些。
可以通过以下五个步骤检索复选框组的选定值
- 在
<form>
标签上设置 ref(如下所示的第 27 行)。 - 从表单中提取复选框组。在本例中,它是
pet
组(第 9 行)。- 这里返回一个节点列表和一个值。
- 请记住,节点列表看起来像数组,但它不是数组,并且缺少数组方法,这将我们带到下一步……
- 将节点列表转换为数组,以便可以使用数组方法(第 12 行的
checkboxArray
)。 - 使用
Array.filter()
仅获取选中的复选框(第 15 行的checkedCheckboxes
)。 - 使用
Array.map()
仅保留选中的复选框的值(第 19 行的checkedCheckboxesValues
)。
import React, { Component } from 'react';
class RefsForm extends Component {
handleSubmit = (e) => {
e.preventDefault();
// extract the node list from the form
// it looks like an array, but lacks array methods
const { pet } = this.form;
// convert node list to an array
const checkboxArray = Array.prototype.slice.call(pet);
// extract only the checked checkboxes
const checkedCheckboxes = checkboxArray.filter(input => input.checked);
console.log('checked array:', checkedCheckboxes);
// use .map() to extract the value from each checked checkbox
const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
console.log('checked array values:', checkedCheckboxesValues);
}
render() {
return (
<div>
<form
onSubmit={this.handleSubmit}
ref={form => this.form = form}>
<label>
Cat
<input type="checkbox" value="cat" name="pet" />
</label>
<label>
Dog
<input type="checkbox" value="dog" name="pet" />
</label>
<label>
Ferret
<input type="checkbox" value="ferret" name="pet" />
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
export default RefsForm;
使用复选框组子组件的工作方式与上一节中的单选按钮组示例相同。
import React, { Component } from 'react';
class RefsForm extends Component {
handleSubmit = (e) => {
e.preventDefault();
// extract the node list from the form
// it looks like an array, but lacks array methods
const { pet } = this.form;
// convert node list to an array
const checkboxArray = Array.prototype.slice.call(pet);
// extract only the checked checkboxes
const checkedCheckboxes = checkboxArray.filter(input => input.checked);
console.log('checked array:', checkedCheckboxes);
// use .map() to extract the value from each checked checkbox
const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
console.log('checked array values:', checkedCheckboxesValues);
}
render() {
return (
<div>
<form
onSubmit={this.handleSubmit}
ref={form => this.form = form}>
<CheckboxSet
setName={'pet'}
setOptions={['cat', 'dog', 'ferret']} />
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
function CheckboxSet(props) {
return (
<div>
{props.setOptions.map(option => {
return (
<label
key={option}
style={{textTransform: 'capitalize'}}>
{option}
<input
type="checkbox"
value={option}
name={props.setName} />
</label>
)
})}
</div>
);
}
export default RefsForm;
结论
如果您不需要
- 实时监控表单元素的值(例如,为了根据用户输入渲染后续组件),或者
- 实时执行自定义验证,
那么使用ref
获取表单元素数据是一个不错的选择。
与受控组件相比,使用ref
的主要价值在于,在大多数情况下,您将编写更少的代码。特殊情况是复选框组(以及在较小程度上单选按钮组)。对于复选框组,使用 refs 可以节省的代码量很少,因此使用受控组件还是ref
并不那么明确。
关于使用受控组件和 refs 的解释很棒。我有两个想法。
A) 在第 2 点中,为什么
RefsForm
应该能够访问CustomInput
的子元素?这看起来像是 React 的反模式。相反,CustomInput
应该提供一个作为回调的 prop,每当其值发生变化时,RefsForm
就可以监听该 prop 以获取更新后的值。B) 还有其他一些通常更简洁的技术来访问不受控的输入,而无需使用每个元素的 ref。
需要明确的是,最后两种方法——使用
FormData
(内置)和formSerialize
(npm 包)——开箱即用地正确处理单选按钮和复选框。第一种方法需要手动处理单选按钮和复选框(如文章中的第 3 点所述)。使用 refs 以外的表单替代方案是使用
onSubmit
处理程序和FormData
类(所有信誉良好的浏览器和 polyfills 中都可用)。虽然不是 React 专家,但我一直觉得 refs 更像是处理不适合 React 虚拟 DOM 的库或代码的应急方案。
使用
FormData
及其 getter(entries()
、getAll()
等)时要小心。虽然大多数浏览器都提供了基本的FormData
功能(例如,将FormData
作为fetch()
的主体传递),但 Safari、IE 和 Edge 中不提供其 getter (!)。我建议使用 form-serialize。
我们只需要监听表单提交,一旦表单提交,将表单实例传递给 form-serialize 以获取 JSON 对象。
通过使用 form-serialize,我们可以避免受控输入,并且不必每次添加输入时都编写 refs。
这很有帮助!jsx 更好地控制数组。这是我的收获 :)
如果您有一大堆输入,将所有 refs 直接分配给
this
可能会变成一个头痛的问题。将它们组织成一个集合(例如this.fields
)可能更容易。<input
type="number"
ref={cashMoney => this.fields.amount = cashMoney} />
然后,如果需要,您可以使用
for in
循环遍历this.fields
来访问所有字段。此外,如果您需要为其他目的在组件上维护一个this.amount
属性,则<input name=amount>
元素不会与之冲突。如果可能通过声明式方式实现,则建议避免使用 ref。Facebook 推荐 ref 的用例很少,对吧?