Dark Dwarf Blog background

React 受控组件

React 受控组件

1. 引入

在 HTML 中,像 <input>, <textarea>, 和 <select> 这样的表单元素通常会自己维护其内部状态,并根据用户的输入自动更新:

<input type="text" name="username" />

当我们在这个输入框里打字时,是 DOM 节点本身在保存和显示这个值。

但是,React 组件无法直接知道或控制这个值。这就导致了状态的分裂:一部分状态在 React 组件里,另一部分则“隐藏”在 DOM 中。这种分裂会使表单的验证、重置和条件逻辑变得复杂和不可预测。

在 React 中,将组件的状态(State)作为“单一数据源”是管理和组织代码的核心思想。受控组件(Controlled Components)正是这一思想在处理表单时的体现。受控组件通过将表单数据完全交给 React 组件来管理,解决了这个问题。

2. 受控组件

a.a. 核心思想

受控组件的核心思想非常简单:让 React 的 state 成为表单元素值的唯一数据源。一个受控组件的工作流程如下:

  1. 状态驱动视图:将 React 组件 state 中的一个变量绑定到表单元素的 value 属性上。
  2. 视图更新状态:为表单元素提供一个 onChange 事件处理函数。当用户输入时,这个函数会触发,并使用用户的输入来更新 React 的 state
  3. 重新渲染state 的更新会触发组件的重新渲染,表单元素会显示更新后的 state 值。

通过这种方式,组件的状态和表单的值始终保持同步,React 组件完全“控制”了表单。这种模式带来了如下的好处:

  1. 单一数据源:组件的 state 包含了整个表单的当前状态,使得调试和状态管理变得清晰、可预测。
  2. 即时验证与反馈:可以在 onChange 处理函数中对用户的每次输入进行实时验证,并立即通过 UI 反馈给用户(例如,显示错误信息或禁用提交按钮)。
  3. 强制格式化:可以轻松实现输入格式化,如信用卡号自动添加空格、输入内容自动转为大写等。
  4. 条件逻辑:可以根据一个输入的值,动态地改变另一个输入的状态(例如,选择“其他”选项时,显示一个文本框)。

b.b. 实现一个受控输入框

下面是一个典型的受控组件实现:

import React, { useState } from "react";

function NameForm() {
  // 1. 创建一个 state 变量来存储输入框的值
  const [name, setName] = useState("");

  // 2. 创建一个 onChange 事件处理函数
  const handleChange = (event) => {
    // event.target.value 包含了输入框的当前文本
    setName(event.target.value.toUpperCase()); // 可以在这里对输入进行格式化
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    alert("A name was submitted: " + name);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        {/* 3. 将 state 绑定到 value,并将处理函数绑定到 onChange */}
        <input type="text" value={name} onChange={handleChange} />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

在这个例子中,name 状态是输入框值的“唯一真相”。无论用户输入什么,输入框显示的内容都由 name 决定。handleChange 函数作为中间人,确保了用户的操作能够正确地更新这个“唯一真相”。

3. 常见的受控组件

受控组件模式可以应用于所有表单元素。

a.a. 文本域

在 HTML 中,<textarea> 的值由其子节点决定。但在 React 中,它和 input 一样,也使用 value 属性。

const [story, setStory] = useState("Once upon a time...");

const handleStoryChange = (event) => {
  setStory(event.target.value);
};

<textarea value={story} onChange={handleStoryChange} />;

b.b. 下拉框

在 React 中,我们不再使用 selected 属性来标记 <option>,而是在根 <select> 标签上使用 value 属性来指定当前选中的值。

const [flavor, setFlavor] = useState("coconut");

const handleFlavorChange = (event) => {
  setFlavor(event.target.value);
};

<select value={flavor} onChange={handleFlavorChange}>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>;

这种方式使得 <select> 标签与其他受控组件的处理方式完全一致,极大地简化了心智模型。

4. 处理多个输入

当一个表单中有多个输入框时,为每个输入框都创建一个单独的 onChange 处理函数会很繁琐。我们可以通过给每个元素添加 name 属性,让一个处理函数动态地管理多个输入。

function Reservation() {
  const [formState, setFormState] = useState({
    isGoing: true,
    numberOfGuests: 2,
  });

  const handleInputChange = (event) => {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;

    // 使用计算属性名 [name] 来动态更新对应的 state
    setFormState({
      ...formState,
      [name]: value,
    });
  };

  return (
    <form>
      <label>
        Is going:
        <input
          name="isGoing"
          type="checkbox"
          checked={formState.isGoing}
          onChange={handleInputChange}
        />
      </label>
      <br />
      <label>
        Number of guests:
        <input
          name="numberOfGuests"
          type="number"
          value={formState.numberOfGuests}
          onChange={handleInputChange}
        />
      </label>
    </form>
  );
}

5. 对比:非受控组件

与受控组件相对的是非受控组件(Uncontrolled Components)。在非受控组件中,表单数据由 DOM 节点本身处理。我们需要使用 ref 来从 DOM 中“拉取”表单的值,通常是在表单提交时。

function UncontrolledForm() {
  const inputRef = React.useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    alert("A name was submitted: " + inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" ref={inputRef} />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

虽然非受控组件在代码上可能更简单,尤其对于非常简单的表单,但它失去了受控组件带来的诸多优势。