说说对受控组件和非受控组件的理解?应用场景?

一、是什么

在 React 中处理表单元素时,根据数据管理方式的不同,组件可以分为两类:

  • 受控组件(Controlled Component):表单元素的值由 React 状态驱动,每次变化都通过事件处理函数更新状态,React 是数据的"唯一真相源"
  • 非受控组件(Uncontrolled Component):表单元素的值由 DOM 自身管理,React 通过 ref 在需要时读取值,DOM 是数据的"真相源"

这两种模式各有优劣,适用于不同的场景。

二、受控组件

受控组件的核心特征是:表单元素的 value 属性绑定到 React 的 state,通过 onChange 事件同步更新。

基础用法

import { useState } from 'react';

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

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

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('提交数据:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="用户名"
      />
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

受控组件的实时校验

受控组件的一大优势是可以在输入过程中实时处理和校验数据:

function RegistrationForm() {
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');

  const validateEmail = (value: string) => {
    if (!value) {
      setEmailError('邮箱不能为空');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setEmailError('邮箱格式不正确');
    } else {
      setEmailError('');
    }
  };

  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    validateEmail(value);
  };

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleEmailChange}
        className={emailError ? 'input-error' : ''}
      />
      {emailError && <span className="error">{emailError}</span>}
    </div>
  );
}

受控组件的输入格式化

受控组件可以轻松实现输入格式化,比如电话号码的自动格式化:

function PhoneInput() {
  const [phone, setPhone] = useState('');

  const formatPhone = (value: string): string => {
    const digits = value.replace(/\D/g, '').slice(0, 11);
    if (digits.length <= 3) return digits;
    if (digits.length <= 7) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
    return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
  };

  return (
    <input
      value={phone}
      onChange={e => setPhone(formatPhone(e.target.value))}
      placeholder="请输入手机号"
    />
  );
}

各种表单元素的受控写法

function FormExample() {
  const [text, setText] = useState('');
  const [selected, setSelected] = useState('option1');
  const [checked, setChecked] = useState(false);
  const [multiSelected, setMultiSelected] = useState<string[]>([]);

  return (
    <form>
      {/* 文本输入 */}
      <input value={text} onChange={e => setText(e.target.value)} />

      {/* 下拉选择 */}
      <select value={selected} onChange={e => setSelected(e.target.value)}>
        <option value="option1">选项一</option>
        <option value="option2">选项二</option>
        <option value="option3">选项三</option>
      </select>

      {/* 复选框 */}
      <label>
        <input
          type="checkbox"
          checked={checked}
          onChange={e => setChecked(e.target.checked)}
        />
        同意条款
      </label>

      {/* 文本域 */}
      <textarea value={text} onChange={e => setText(e.target.value)} />
    </form>
  );
}

三、非受控组件

非受控组件将数据存储在 DOM 中,通过 ref 访问表单值。使用 defaultValue(而非 value)设置初始值。

基础用法

import { useRef } from 'react';

function UncontrolledForm() {
  const usernameRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const username = usernameRef.current?.value;
    const password = passwordRef.current?.value;
    console.log('提交数据:', { username, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={usernameRef} defaultValue="" placeholder="用户名" />
      <input ref={passwordRef} type="password" defaultValue="" placeholder="密码" />
      <button type="submit">登录</button>
    </form>
  );
}

文件上传——天然的非受控组件

<input type="file" /> 是一个典型的非受控组件,因为它的值只能由用户设置,不能通过程序设定:

function FileUpload() {
  const fileRef = useRef<HTMLInputElement>(null);

  const handleUpload = async () => {
    const files = fileRef.current?.files;
    if (!files || files.length === 0) return;

    const formData = new FormData();
    Array.from(files).forEach(file => {
      formData.append('files', file);
    });

    await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });
  };

  return (
    <div>
      <input ref={fileRef} type="file" multiple accept="image/*" />
      <button onClick={handleUpload}>上传</button>
    </div>
  );
}

defaultValue vs value

function DefaultValueExample() {
  return (
    <div>
      {/* ✅ 非受控:设置初始值,之后 DOM 自行管理 */}
      <input defaultValue="初始文本" />

      {/* ❌ 受控但没有 onChange:输入框会变成只读 */}
      {/* React 会发出警告 */}
      {/* <input value="固定文本" /> */}

      {/* ✅ 如果确实需要只读的受控输入 */}
      <input value="只读文本" readOnly />
    </div>
  );
}

四、React 19 中的表单处理

React 19 引入了 Form Actions,提供了一种新的表单处理方式:

import { useActionState } from 'react';

async function submitForm(prevState: any, formData: FormData) {
  const username = formData.get('username') as string;
  const password = formData.get('password') as string;

  if (!username || !password) {
    return { error: '用户名和密码不能为空' };
  }

  const res = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ username, password }),
    headers: { 'Content-Type': 'application/json' },
  });

  if (!res.ok) {
    return { error: '登录失败' };
  }

  return { success: true };
}

function LoginForm() {
  const [state, formAction, isPending] = useActionState(submitForm, null);

  return (
    <form action={formAction}>
      <input name="username" placeholder="用户名" required />
      <input name="password" type="password" placeholder="密码" required />
      {state?.error && <p className="error">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

这种方式结合了非受控组件的简洁性和服务端交互的便利性,特别适合与 Server Components 配合使用。

五、useFormStatus

React 19 还提供了 useFormStatus Hook,用于获取表单提交状态:

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  );
}

function ContactForm() {
  async function handleSubmit(formData: FormData) {
    'use server';
    const name = formData.get('name');
    const message = formData.get('message');
    await saveMessage({ name, message });
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="姓名" />
      <textarea name="message" placeholder="留言内容" />
      <SubmitButton />
    </form>
  );
}

六、对比总结

特性受控组件非受控组件
数据管理React stateDOM
取值方式直接读取 state通过 ref 读取
初始值设置value + onChangedefaultValue
实时校验✅ 方便❌ 困难
输入格式化✅ 方便❌ 困难
动态禁用提交✅ 方便❌ 需额外逻辑
性能(大表单)每次输入触发渲染无额外渲染
代码量较多较少
测试便利性✅ 状态可预测需要模拟 DOM

七、应用场景建议

适合受控组件的场景

  • 需要实时校验的表单(注册、登录)
  • 需要输入格式化(手机号、银行卡号)
  • 需要根据输入联动其他 UI(搜索建议、动态表单)
  • 需要程序化控制表单值(重置、填充默认值)

适合非受控组件的场景

  • 简单的一次性表单(搜索框、评论输入)
  • 文件上传
  • 需要集成第三方 DOM 库
  • 大型表单追求极致性能(配合表单库如 React Hook Form)
// React Hook Form 采用非受控 + ref 的方式,性能优异
import { useForm } from 'react-hook-form';

interface FormValues {
  username: string;
  email: string;
  age: number;
}

function PerformantForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>();

  const onSubmit = (data: FormValues) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username', { required: '用户名必填' })} />
      {errors.username && <span>{errors.username.message}</span>}

      <input {...register('email', {
        required: '邮箱必填',
        pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式错误' }
      })} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="number" {...register('age', { min: { value: 0, message: '年龄不能为负' } })} />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit">提交</button>
    </form>
  );
}

八、总结

受控组件和非受控组件代表了 React 中管理表单数据的两种哲学。受控组件让 React 完全掌控数据,适合需要实时交互的场景;非受控组件让 DOM 管理数据,适合简单表单和性能敏感场景。React 19 的 Form Actions 则提供了第三种选择,结合了两者的优点。在实际项目中,应根据具体需求灵活选择,甚至在同一个表单中混合使用。