译文:React 中的数据绑定

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

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

这取决于表单控件的类型:文本输入、选择框、复选框和单选按钮都有一些不同的工作方式。

好消息是,尽管细节各不相同,但它们都共享相同的基本机制。在 React 中,数据绑定方面有一致的哲学。

在本教程中,我们首先将学习 React 如何处理数据绑定,然后我将逐一向您展示每个表单字段的工作原理。我们将查看完整的实际示例。

Introduction to controlled fields 受控字段简介

假设我们渲染一个 <input>

jsx 复制代码
function App() {
  return (
    <input />
  );
}

默认情况下,React采取了一种非常"不干涉"的方式。它为我们创建了 DOM 节点,然后就不再干涉了。这被称为非受控元素,因为 React 不主动管理它。

然而,作为替代,我们可以选择让 React 来管理表单字段。对于文本输入,我们可以使用 value 属性进行选择:

jsx 复制代码
import React from 'react';

function App() {
  return (
    <input value="Hello World" />
  );
}

export default App;

尝试编辑输入框中的文本。它不起作用!

这被称为受控元素。React 会保持警惕,确保输入始终显示字符串"Hello World"。

现在,将 value 锁定为静态字符串并没有太大用处!我在这里这样做纯粹是为了说明受控元素的工作原理:React"锁定"输入,使其始终包含我们传入的 value 。

真正的魔力在于传递动态值。让我们看另一个例子:

jsx 复制代码
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 状态变量的值上。

在数据绑定术语中,这被称为"单向"数据绑定。当状态改变时,输入会更新,但当输入被编辑时,状态不会更新。

为了完成循环,我们需要双向数据绑定。以下是我们实现它的方法:

jsx 复制代码
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 状态:

jsx 复制代码
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 输入:

jsx 复制代码
const [secret, setSecret] = React.useState('');

<input
  type="password"
  value={secret}
  onChange={(event) => {
    setSecret(event.target.value);
  }}
/>

除了文本输入变体之外, <input> 标签还可以变形成完全独立的表单控件。

在本博客文章的后面,我们将讨论单选按钮、复选框以及滑块和颜色选择器等特殊输入方式。

Gotchas 注意事项

在处理文本输入时,请确保将空字符串( '' )作为初始状态:

jsx 复制代码
// 🚫 Incorrect:
const [name, setName] = React.useState();

// ✅ Correct:
const [name, setName] = React.useState('');

Textareas 文本区域

在 React 中, <textarea> 元素的工作方式与文本输入框完全相同。我们使用相同的组合 value + onChange :

jsx 复制代码
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 注意事项

与输入一样,确保将空字符串( '' )作为状态变量的初始值

jsx 复制代码
// 🚫 Incorrect:
const [comment, setComment] = React.useState();

// ✅ Correct:
const [comment, setComment] = React.useState('');

Radio buttons 单选按钮

当涉及到单选按钮时,情况有些不同!

让我们从一个例子开始:

jsx 复制代码
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 处理程序中看到这一点

jsx 复制代码
<input
  value="yes"
  onChange={(event) => {
    setHasAgreed(event.target.value);
    // Equivalent to: setHasAgreed("yes")
  }}
/>

当用户勾选此特定输入(代表"yes"选项)时,我们将该"yes"值复制到状态中。

为了实现真正的双向数据绑定,我们需要将其变为受控输入。在 React 中,单选按钮通过 checked 属性进行控制:

jsx 复制代码
<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 获取)。在这些情况下,我们需要使用迭代生成它们。

以下是示例:

jsx 复制代码
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 复选框

复选框与单选按钮非常相似,但它们也有自己的复杂性。

我们的策略将取决于我们是在讨论单个复选框还是一组复选框。

让我们从一个基本的例子开始,只使用一个复选框:

jsx 复制代码
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 状态来控制多个复选框时,情况会变得更加复杂。

让我们来看一个例子。通过勾选不同的复选框并观察其对应的状态变化,看看你能否理解这里发生了什么:

jsx 复制代码
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 。但是对于复选框,我们需要存储更多数据,因为用户可以选择多个选项。

有很多方法可以做到这一点。我最喜欢的方法是使用一个对象,为每个选项保存一个布尔值:

jsx 复制代码
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>

这是一个示例,展示了如何将其绑定到状态变量:

jsx 复制代码
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 注意事项

与文本输入类似,我们需要将状态初始化为有效值。这意味着我们的状态变量的初始值必须与选项之一匹配。

jsx 复制代码
// 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> 标签,使用一个统一的数据源:

jsx 复制代码
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"> 的例子:

jsx 复制代码
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">

jsx 复制代码
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。以下是它的样子:

jsx 复制代码
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 进行连接。

相关推荐
Domain-zhuo9 分钟前
Git常用命令
前端·git·gitee·github·gitea·gitcode
菜根Sec39 分钟前
XSS跨站脚本攻击漏洞练习
前端·xss
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工1 小时前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲2 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
Anlici2 小时前
three.js建立3D模型展示地球+高亮
前端·数据可视化·canvas
轻口味2 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami2 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda3 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡3 小时前
lodash常用函数
前端·javascript