译文: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 进行连接。

相关推荐
90后的晨仔6 小时前
在macOS上无缝整合:为Claude Code配置魔搭社区免费API完全指南
前端
沿着路走到底7 小时前
JS事件循环
java·前端·javascript
子春一27 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶7 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn8 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪8 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ9 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied9 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一29 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
前端无涯9 小时前
React/Vue 代理配置全攻略:Vite 与 Webpack 实战指南
vue.js·react.js