2、受控组件实战:使用useState管理表单输入,实现双向数据绑定

好,咱们直接进入正题。

上一章我们聊了受控组件和非受控组件的区别。说白了,受控组件就是把表单的「状态」交给 React 来管。那怎么管?最朴素的方式就是——useState

我个人习惯把受控组件叫做「React 说了算的输入框」。你想想看,用户在输入框里敲一个字,这个字先经过 React 的 state,再渲染回输入框。这就是所谓的「双向数据绑定」。

2.1 从零开始:一个受控的输入框

先看最基础的例子。一个用户名输入框,用 useState 来管理它的值。

import React, { useState } from 'react';

function LoginForm() {
  const [username, setUsername] = useState('');

  const handleChange = (e) => {
    setUsername(e.target.value);
  };

  return (
    <div>
      <label>用户名:</label>
      <input
        type="text"
        value={username}
        onChange={handleChange}
      />
      <p>你输入的是:{username}</p>
    </div>
  );
}

这段代码里,value={username} 是数据流下去的管道,onChange={handleChange} 是事件传上来的通道。两者缺一不可。

核心要点:受控组件必须同时设置 value 和 onChange。少一个,输入框要么不能输入,要么不受 React 控制。

我在项目中遇到过不少新手,只写了 value 没写 onChange,然后跑来问我「为什么输入框点不动?」——嗯,因为 React 把值锁死了,没有 onChange 去更新 state,输入框永远显示初始值。

2.2 处理多个输入字段

实际项目中,一个表单不可能只有一个字段。那怎么管理多个输入?

我建议用一个对象来存所有字段的值。这样代码更干净,也方便扩展。

function RegisterForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />
      <input name="password" type="password" value={formData.password} onChange={handleChange} />
    </form>
  );
}

注意这里的关键:name 属性必须和 state 里的 key 对应。这样所有输入框可以共用同一个 handleChange 函数。

小技巧:使用 setFormData(prev => ({...prev, [name]: value})) 这种函数式更新,可以避免因闭包导致的 stale state 问题。我吃过这个亏,后来就养成了习惯。

2.3 处理不同类型的输入

不是所有输入都是文本框。复选框、单选按钮、下拉菜单,它们的取值方式不一样。

输入类型 取值属性 示例
text / email / password e.target.value 普通文本输入
checkbox e.target.checked 布尔值
radio e.target.value 选中的值
select e.target.value 选中的 option 值

举个例子,处理复选框:

const [agree, setAgree] = useState(false);

const handleCheckbox = (e) => {
  setAgree(e.target.checked);
};

// 渲染
<input type="checkbox" checked={agree} onChange={handleCheckbox} />

这里用的是 checked 而不是 value。我刚开始做 React 时也搞混过,checkbox 用 value 去绑定,结果永远取不到正确的值。

2.4 双向数据绑定的本质

很多人听到「双向数据绑定」就想到 Angular 或者 Vue。其实 React 里的双向绑定是模拟出来的。

它的流程是这样的:

  1. 用户输入 → 触发 onChange 事件
  2. onChange 更新 state
  3. state 变化 → 组件重新渲染
  4. 新的 value 被设置到输入框

你看,这其实是一个单向循环。数据从 state 流向 UI,再从 UI 流回 state。只是这个循环转得足够快,用户感觉是「双向」的。

避坑指南:千万不要在 onChange 里直接修改 state 对象。比如 formData.username = e.target.value 这种操作,React 检测不到变化,不会重新渲染。我曾经在一个紧急项目里犯过这个错,排查了半天才发现是直接修改了对象。

2.5 实战:一个完整的登录表单

把上面的知识点串起来,写一个完整的登录表单:

function LoginForm() {
  const [form, setForm] = useState({
    email: '',
    password: '',
    rememberMe: false
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const val = type === 'checkbox' ? checked : value;

    setForm(prev => ({
      ...prev,
      [name]: val
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // 这里做验证,下一章会详细讲
    console.log('提交的数据:', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>邮箱:</label>
        <input name="email" type="email" value={form.email} onChange={handleChange} />
      </div>
      <div>
        <label>密码:</label>
        <input name="password" type="password" value={form.password} onChange={handleChange} />
      </div>
      <div>
        <label>
          <input name="rememberMe" type="checkbox" checked={form.rememberMe} onChange={handleChange} />
          记住我
        </label>
      </div>
      <button type="submit">登录</button>
    </form>
  );
}

这个例子把 text、password、checkbox 三种类型统一处理了。核心就是根据 type 判断取 value 还是 checked

2.6 什么时候不用受控组件?

说了这么多受控组件的好处,我也得坦白讲:不是所有场景都适合用受控组件

比如:

  • 文件上传(<input type="file" />)—— 它的值是只读的,没法受控
  • 简单的、不需要实时验证的搜索框
  • 第三方编辑器(如富文本编辑器)

这些场景用非受控组件或者 ref 会更合适。我个人的原则是:只要需要实时获取输入值、做验证、或者联动其他组件,就用受控组件。否则,怎么简单怎么来。

总结一下:useState 管理表单输入,核心就是三件事——存值、取值、更新值。把这三件事理清楚,受控组件你就掌握了八成。

下一章我们会在这个基础上,加上验证逻辑。到时候你会发现,受控组件做验证简直不要太方便——因为值就在 state 里,随时可以检查。