原文链接:Data binding in React: how to work with forms in React (joshwcomeau.com)

假如你在 React 中有一些状况,而且你想将其与表单字段同步。你该怎么做?

译文: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 状况变量的值上。
在数据绑定术语中,这被称为“单向”数据绑定。当状况改动时,输入会更新,但当输入被修改时,状况不会更新。

译文:React 中的数据绑定

为了完结循环,咱们需求双向数据绑定。以下是咱们完成它的办法:

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 中的数据绑定

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 进行衔接。