原文链接:Data binding in React: how to work with forms in React (joshwcomeau.com)
假如你在 React 中有一些状况,而且你想将其与表单字段同步。你该怎么做?
这取决于表单控件的类型:文本输入、挑选框、复选框和单选按钮都有一些不同的作业办法。
好消息是,虽然细节各不相同,但它们都共享相同的根本机制。在 React 中,数据绑定方面有共同的哲学。
在本教程中,咱们首先将学习 React 怎么处理数据绑定,然后我将逐一向您展现每个表单字段的作业原理。咱们将查看完整的实际示例。
Introduction to controlled fields 受控字段简介
假设咱们渲染一个 <input>
:
function App() {
return (
<input />
);
}
默许状况下,React采取了一种十分“不干与”的办法。它为咱们创建了 DOM 节点,然后就不再干与了。这被称为非受控元素,由于 React 不主动管理它。
可是,作为替代,咱们能够挑选让 React 来管理表单字段。关于文本输入,咱们能够运用 value 特点进行挑选:
import React from 'react';
function App() {
return (
<input value="Hello World" />
);
}
export default App;
测验修改输入框中的文本。它不起作用!
这被称为受控元素。React 会保持警惕,确保输入一直显现字符串“Hello World”。
现在,将 value 确定为静态字符串并没有太大用处!我在这儿这样做纯粹是为了阐明受控元素的作业原理:React“确定”输入,使其一直包括咱们传入的 value 。
真正的魔力在于传递动态值。让咱们看另一个比如:
import React from 'react';
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<input value={count} />
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</>
);
}
export default App;
试着点击“添加”按钮,注意一下文本输入框会产生什么改动。
咱们没有将输入框绑定到一个静态字符串,而是绑定到一个状况变量 count 。当咱们点击“添加”按钮时,这个状况变量从 0 变为 1 。React 从头渲染这个组件,并更新 <input>
中的值以反映这个新的状况。
不过,咱们仍然无法在文本输入框中输入文字!React 将输入框确定在 count 状况变量的值上。
在数据绑定术语中,这被称为“单向”数据绑定。当状况改动时,输入会更新,但当输入被修改时,状况不会更新。
为了完结循环,咱们需求双向数据绑定。以下是咱们完成它的办法:
import React from 'react';
function App() {
const [state, setState] = React.useState('Hello World');
return (
<>
<input
value={state}
onChange={(event) => {
setState(event.target.value);
}}
/>
<p>
<strong>Current value:</strong>
{state}
</p>
</>
);
}
export default App;
咱们运用 onChange 特点附加一个工作监听器。当用户修改文本输入时,该函数被调用,并传入 event 目标。
event.target 是指触发工作的 DOM 节点:在这种状况下,它是文本输入框。该文本输入框具有 value 特点,它表示用户刚刚测验输入到输入框中的值。
咱们更新 React 状况,使其保存这个新值。React 从头渲染,并将该新值推送到输入框中。循环完结!
这是 React 中数据绑定的根本思想。两个要素是:
- 一个“受控”字段,将输入框确定到 React 状况的一部分。
- 当用户修改输入时,一个处理程序会更新状况变量。
有了这个衔接,咱们就有了正确的双向数据绑定。
React 的中心理念之一是 UI 是由状况派生的。当状况产生改动时,UI 会从头绘制以匹配新的状况。受控元素是这个理念的自然延伸。例如,经过为文本输入指定一个 value ,咱们就是在说输入框的内容也是由 React 状况派生的。
好的,让咱们来看看这个方式怎么应用于不同的输入类型。
Text inputs 文本输入
这是一个更完整的示例,将文本输入绑定到 React 状况:
import React from 'react';
function App() {
const [name, setName] = React.useState('');
return (
<>
<form>
<label htmlFor="name-field">
Name:
</label>
<input
id="name-field"
value={name}
onChange={event => {
setName(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{name || '(empty)'}
</p>
</>
);
}
export default App;
这儿的两个关键特点是 value 和 onChange :
- value 将输入确定,强制一直显现咱们状况变量的当前值。
- onChange 在用户修改输入时触发,并更新状况。
我还提供了一个 id 。这不是数据绑定所必需的,但它是一个重要的可用性和可拜访性要求。ID 需求是大局仅有的;稍后,咱们将学习怎么运用新的 React 钩子主动生成它们。
Text input variants 文本输入变体
除了普通文本输入外,咱们还能够挑选不同的“格式化”文本输入,用于电子邮件地址、电话号码和密码等内容。
好消息是:就数据绑定而言,一切这些变体都以相同的办法作业。
例如,这是咱们怎么绑定一个 password 输入:
const [secret, setSecret] = React.useState('');
<input
type="password"
value={secret}
onChange={(event) => {
setSecret(event.target.value);
}}
/>
除了文本输入变体之外, <input>
标签还能够变构成完全独立的表单控件。
在本博客文章的后边,咱们将评论单选按钮、复选框以及滑块和色彩挑选器等特别输入办法。
Gotchas 注意事项
在处理文本输入时,请确保将空字符串( ” )作为初始状况:
// Incorrect:
const [name, setName] = React.useState();
// ✅ Correct:
const [name, setName] = React.useState('');
Textareas 文本区域
在 React 中, <textarea>
元素的作业办法与文本输入框完全相同。咱们运用相同的组合 value + onChange :
import React from 'react';
function App() {
const [comment, setComment] = React.useState('');
return (
<>
<form>
<label htmlFor="comment-field">
Share your experiences:
</label>
<textarea
id="comment-field"
value={comment}
onChange={event => {
setComment(
event.target.value
);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{comment || '(empty)'}
</p>
</>
);
}
export default App;
Gotchas 注意事项
与输入相同,确保将空字符串( ” )作为状况变量的初始值
// Incorrect:
const [comment, setComment] = React.useState();
// ✅ Correct:
const [comment, setComment] = React.useState('');
Radio buttons 单选按钮
当涉及到单选按钮时,状况有些不同!
让咱们从一个比如开端:
import React from 'react';
function App() {
const [hasAgreed, setHasAgreed] = React.useState();
return (
<>
<form>
<fieldset>
<legend>
Do you agree?
</legend>
<input
type="radio"
name="agreed-to-terms"
id="agree-yes"
value="yes"
checked={hasAgreed === "yes"}
onChange={event => {
setHasAgreed(event.target.value)
}}
/>
<label htmlFor="agree-yes">
Yes
</label>
<br />
<input
type="radio"
name="agreed-to-terms"
id="agree-no"
value="no"
checked={hasAgreed === "no"}
onChange={event => {
setHasAgreed(event.target.value)
}}
/>
<label htmlFor="agree-no">
No
</label>
</fieldset>
</form>
<p>
<strong>Has agreed:</strong>
{hasAgreed || "undefined"}
</p>
</>
);
}
export default App;
首先,我想解释一下咱们的“受控字段”策略在这儿的应用。
运用文本输入时,咱们的状况和表单控件之间存在一对一的联系。一个状况只绑定一个 <input>
标签。
而关于单选按钮,多个输入绑定到一个状况上!这是一对多的联系。正由于这个差异,所以界面看起来如此不同。
在上面的示例中,咱们的状况一直等于三个或许的值之一:
- undefined (未挑选选项)
- “yes” (第一个单选按钮的 value )
- “no” (第二个单选按钮的 value )
咱们的状况变量盯梢的是哪个选项被选中,而不是追踪特定输入的值。
咱们能够在 onChange 处理程序中看到这一点
<input
value="yes"
onChange={(event) => {
setHasAgreed(event.target.value);
// Equivalent to: setHasAgreed("yes")
}}
/>
当用户勾选此特定输入(代表“yes”选项)时,咱们将该“yes”值复制到状况中。
为了完成真正的双向数据绑定,咱们需求将其变为受控输入。在 React 中,单选按钮经过 checked 特点进行操控:
<input
value="yes"
checked={hasAgreed === "yes"}
/>
经过为 checked 指定一个布尔值,React 将主动管理此单选按钮,根据 hasAgreed === “yes” 表达式来选中或撤销选中 DOM 节点。
很不幸的是,文本输入和单选按钮在建立受控输入方面依赖不同的特点( value vs. checked )。这导致了许多混淆。
可是当咱们考虑到 React 实际上操控的是什么时,这种做法有点合理:
- 关于文本输入,React 操控用户输入的自在文本(用 value 指定)。
- 关于单选按钮,React 操控用户是否挑选了此特定选项(用 checked 指定)。
其他特点呢?下面是一个表格,显现每个特点的责任:
Attribute | Type | Explanation |
---|---|---|
id | string | 一个大局仅有的标识符,用于改进单选按钮的可拜访性和可用性。 |
name | string | 将一组单选按钮组合在一起,以便一次只能挑选一个。组内一切单选按钮的值必须相同。 |
value | string | 指定此单选按钮所代表的“事物”。假如挑选了此特定选项,将捕获/存储此事物。 |
checked | boolean | 操控单选按钮是否被选中。经过传递一个布尔值,React 将使其成为“受控”输入。 |
onChange | function | 与其他表单控件相同,当用户更改所选选项时,将调用此函数。咱们运用此函数来更新咱们的状况。 |
Iterative example 迭代示例
由于单选按钮需求许多特点,一般最好运用迭代动态生成它们。这样,咱们只需求编写一次一切的内容!
此外,在许多状况下,选项本身也是动态的(例如从后端 API 获取)。在这些状况下,咱们需求运用迭代生成它们。
以下是示例:
import React from 'react';
function App() {
const [
language,
setLanguage
] = React.useState('english');
return (
<>
<form>
<fieldset>
<legend>
Select language:
</legend>
{VALID_LANGUAGES.map(option => (
<div key={option}>
<input
type="radio"
name="current-language"
id={option}
value={option}
checked={option === language}
onChange={event => {
setLanguage(event.target.value);
}}
/>
<label htmlFor={option}>
{option}
</label>
</div>
))}
</fieldset>
</form>
<p>
<strong>Selected language:</strong>
{language || "undefined"}
</p>
</>
);
}
const VALID_LANGUAGES = [
'mandarin',
'spanish',
'english',
'hindi',
'arabic',
'portugese',
];
export default App;
这或许看起来愈加复杂一些,但终究,一切的特点都是以完全相同的办法运用的。
Checkboxes 复选框
复选框与单选按钮十分相似,但它们也有自己的复杂性。
咱们的策略将取决于咱们是在评论单个复选框还是一组复选框。
让咱们从一个根本的比如开端,只运用一个复选框:
import React from 'react';
function App() {
const [optIn, setOptIn] = React.useState(false);
return (
<>
<form>
<input
type="checkbox"
id="opt-in-checkbox"
checked={optIn}
onChange={event => {
setOptIn(event.target.checked);
}}
/>
<label htmlFor="opt-in-checkbox">
<strong>Yes,</strong> I would like to join the newsletter.
</label>
</form>
<p>
<strong>Opt in:</strong> {optIn.toString()}
</p>
</>
);
}
export default App;
与单选按钮相同,咱们运用 checked 特点来指定这应该是一个受控输入。这使咱们能够将复选框是否被选中与咱们的 optIn 状况变量同步。当用户切换复选框时,咱们运用了解的 onChange 方式更新 optIn 状况。
Checkbox groups 复选框组
当咱们想要用 React 状况来操控多个复选框时,状况会变得愈加复杂。
让咱们来看一个比如。经过勾选不同的复选框并观察其对应的状况改动,看看你能否了解这儿产生了什么:
import React from 'react';
const initialToppings = {
anchovies: false,
chicken: false,
tomatoes: false,
}
function App() {
const [
pizzaToppings,
setPizzaToppings
] = React.useState(initialToppings);
// Get a list of all toppings.
// ['anchovies', 'chicken', 'tomato'];
const toppingsList = Object.keys(initialToppings);
return (
<>
<form>
<fieldset>
<legend>
Select toppings:
</legend>
{/*
Iterate over those toppings, and
create a checkbox for each one:
*/}
{toppingsList.map(option => (
<div key={option}>
<input
type="checkbox"
id={option}
value={option}
checked={pizzaToppings[option] === true}
onChange={event => {
setPizzaToppings({
...pizzaToppings,
[option]: event.target.checked,
})
}}
/>
<label htmlFor={option}>
{option}
</label>
</div>
))}
</fieldset>
</form>
<p>
<strong>Stored state:</strong>
</p>
<p className="output">
{JSON.stringify(pizzaToppings, null, 2)}
</p>
</>
);
}
export default App;
就HTML特点而言,状况与咱们迭代式单选按钮的办法相似…可是咱们的React状况到底是怎么回事?为什么它是一个目标?!
与单选按钮不同,能够挑选多个复选框。这在涉及咱们的状况变量时会改动工作。
关于单选按钮,咱们能够将需求知道的一切内容放入一个字符串中:所选选项的 value 。可是关于复选框,咱们需求存储更多数据,由于用户能够挑选多个选项。
有许多办法能够做到这一点。我最喜爱的办法是运用一个目标,为每个选项保存一个布尔值:
const initialToppings = {
anchovies: false,
chicken: false,
tomatoes: false,
}
在JSX中,咱们遍历这个目标的键,并为每个键渲染一个复选框。在迭代过程中,咱们查找这个特定选项是否被选中,并运用它来操控带有 checked 特点的复选框。
咱们还将一个函数传递给 onChange ,该函数将翻转所评论的复选框的值。由于 React 状况需求是不可变的,咱们经过创建一个简直相同的新目标来处理这个问题,其中所评论的选项在 true/false 之间翻转。
下面是一个显现每个特点用处的表格:
Attribute | Type | Explanation |
---|---|---|
id | string | 用于改进可拜访性和可用性的大局仅有标识符,用于此复选框。 |
value | string | 指定咱们用这个复选框来勾选和撤销勾选的“事物”。 |
checked | boolean | 操控复选框是否被选中。 |
onChange | function | 与其他表单控件相似,当用户勾选或撤销勾选复选框时,将调用此函数。咱们运用此函数来更新咱们的状况。 |
咱们也能够指定一个 name ,就像单选按钮相同,虽然在运用受控输入时这并不是严格必要的。
Select 挑选
与单选按钮相似, <select>
标签答应用户从一组或许的值中挑选一个选项。一般在选项太多以至于无法舒适地运用单选按钮时,咱们会运用 <select>
。
这是一个示例,展现了怎么将其绑定到状况变量:
import React from 'react';
function App() {
const [age, setAge] = React.useState('0-18');
return (
<>
<form>
<label htmlFor="age-select">
How old are you?
</label>
<select
id="age-select"
value={age}
onChange={event => {
setAge(event.target.value)
}}
>
<option value="0-18">
18 and under
</option>
<option value="19-39">
19 to 39
</option>
<option value="40-64">
40 to 64
</option>
<option value="65-infinity">
65 and over
</option>
</select>
</form>
<p>
<strong>Selected value:</strong>
{age}
</p>
</>
);
}
export default App;
在 React 中,<select>
标签与文本输入十分相似。咱们运用相同的 value + onChange 组合。
假如你在原生 JS 中运用过 <select>
标签,这或许看起来有点张狂。一般状况下,咱们需求动态设置适当的 <option>
子元素上的 selected 特点。React 团队在 <select>
方面做了许多改进,去除了粗糙的边缘,让咱们能够运用了解的 value + onChange 组合将这个表单字段绑定到一些 React 状况上。
话虽如此,咱们仍然需求创建 <option>
个子项,并为每个子项指定适当的值。当用户挑选不同的选项时,这些字符串将被设置到状况中。
Gotchas 注意事项
与文本输入相似,咱们需求将状况初始化为有效值。这意味着咱们的状况变量的初始值必须与选项之一匹配。
// This initial value:
const [age, setAge] = React.useState("0-18");
// Must match one of the options:
<select>
<option
value="0-18"
>
18 and under
</option>
</select>
我更喜爱动态生成 <option>
标签,运用一个一致的数据源:
import React from 'react';
// The source of truth!
const OPTIONS = [
{
label: '18 and under',
value: '0-18'
},
{
label: '19 to 39',
value: '19-39'
},
{
label: '40 to 64',
value: '40-64'
},
{
label: '65 and over',
value: '65-infinity'
},
];
function App() {
// Grab the first option from the array.
// Set its value into state:
const [age, setAge] = React.useState(OPTIONS[0].value);
return (
<>
<form>
<label htmlFor="age-select">
How old are you?
</label>
<select
id="age-select"
value={age}
onChange={event => {
setAge(event.target.value)
}}
>
{/*
Iterate over that array, to create
the <option> tags dynamically:
*/}
{OPTIONS.map(option => (
<option
key={option.value}
value={option.value}
>
{option.label}
</option>
))}
</select>
</form>
<p>
<strong>Selected value:</strong>
{age}
</p>
</>
);
}
export default App;
Specialty inputs 特征输入
正如咱们所见, <input>
HTML 标签能够有许多不同的方式。根据 type 特点的不同,它能够是文本输入框、密码输入框、复选框、单选按钮…
事实上,MDN 列出了 type 特点的 22 个不同有效值。其中一些是“特别”的,具有独特的外观:
- Sliders (with type=”range”)
- Date pickers (with type=”date”)
- Color pickers (with type=”color”)
幸运的是,它们都遵循与文本输入相同的方式。咱们运用 value 将输入确定到状况的值,并运用 onChange 在修改输入时更新该值。
这是一个运用 <input type="range">
的比如:
import React from 'react';
function App() {
const [volume, setVolume] = React.useState(50);
return (
<>
<form>
<label htmlFor="volume-slider">
Audio volume:
</label>
<input
type="range"
id="volume-slider"
min={0}
max={100}
value={volume}
onChange={event => {
setVolume(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{volume}
</p>
</>
);
}
export default App;
这是另一个比如,带有 <input type="color">
:
import React from 'react';
function App() {
const [color, setColor] = React.useState('#FF0000');
return (
<>
<form>
<label htmlFor="color-picker">
Select a color:
</label>
<input
type="color"
id="color-picker"
value={color}
onChange={event => {
setColor(event.target.value);
}}
/>
</form>
<p>
<strong>Current value:</strong>
{color}
</p>
</>
);
}
export default App;
Generating unique IDs 生成仅有的ID
在咱们看到的每个比如中,咱们的表单字段都被赋予了一个 id 特点。这个 ID 仅有地标识了该字段,而且咱们运用它来衔接一个 <label>
标签,运用 htmlFor 进行链接(React 版别的“for”特点)。
这个很重要,有两个原因:
- 可拜访性。表单字段需求标签;没有标签,用户怎么知道要输入什么?关于运用屏幕阅读器的人来说,需求正确衔接以确保他们知道每个给定表单字段的标签。
- 可用性。衔接标签运用户能够点击文本来聚集/触发表单控件。这关于单选按钮和复选框特别方便,由于它们一般太小而难以点击。
为了让一切正常运作, id 特点应该是大局仅有的。咱们不答应有多个具有相同 ID 的表单字段。
可是!React 的中心准则之一是可重用性。咱们或许希望在同一页上屡次渲染包括表单字段的组件!
为了协助咱们处理这个难题,React 团队最近推出了一个新的钩子:useId。以下是它的姿态:
import React from 'react';
function LoginForm() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const id = React.useId();
const usernameId = `${id}-username`;
const passwordId = `${id}-password`;
return (
<>
<form>
<div>
<label htmlFor={usernameId}>
Username:
</label>
<input
id={usernameId}
value={username}
onChange={event => {
setUsername(event.target.value);
}}
/>
</div>
<div>
<label htmlFor={passwordId}>
Password:
</label>
<input
id={passwordId}
type="password"
value={password}
onChange={event => {
setPassword(event.target.value);
}}
/>
</div>
<button>
Login
</button>
</form>
</>
);
}
export default LoginForm;
每当咱们渲染这个 LoginForm 组件时,React 会生成一个新的、确保仅有的 ID。您能够在新的 React 文档(new React docs.)中了解更多关于这个钩子的信息。
运用嵌套处理问题? 不只能够提供仅有的 ID,还能够经过嵌套将标签和表单字段进行相关: 我不是无障碍专家,但我听说这种结构并不适用于一切屏幕阅读器。现已确立的最佳实践是运用 ID 进行衔接。