React 中的受控组件与非受控组件详解

94 阅读4分钟

React 中的受控组件与非受控组件详解

在 React 开发中,表单处理是一个常见但又容易混淆的话题。开发者常常会遇到“受控组件”(Controlled Components)和“非受控组件”(Uncontrolled Components)这两个概念。它们代表了两种不同的表单数据管理方式,各有适用场景和优劣。本文将深入解析这两种模式,并结合 useRefuseState 等 Hooks,帮助你掌握何时使用哪种方式。


一、什么是受控组件?

受控组件是指表单元素的值由 React 的状态(state)控制。换句话说,表单元素的 value 属性绑定到某个 state 变量上,并通过 onChange 事件同步更新该 state。

特点:

  • 表单的值完全由 React 状态驱动(单向数据流)。
  • 每次用户输入都会触发状态更新,进而重新渲染组件。
  • 适合需要实时校验、联动、动态展示等复杂交互的场景。

示例:受控登录表单

import { useState } from 'react';

export default function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: ''
  });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(form); // { username: '...', password: '...' }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="请输入用户名"
        name="username"
        value={form.username}
        onChange={handleChange}
      />
      <input
        type="password"
        placeholder="请输入密码"
        name="password"
        value={form.password}
        onChange={handleChange}
      />
      <button type="submit">注册</button>
    </form>
  );
}

在这个例子中,<input> 的值始终由 form 状态决定,任何输入都会通过 handleChange 更新状态,实现双向绑定的效果(虽然 React 本质是单向数据流)。


二、什么是非受控组件?

非受控组件则是让 DOM 自己管理表单数据,React 不直接控制其值。通常通过 ref 来在需要时读取当前的 DOM 值。

特点:

  • 表单元素的值存储在 DOM 中,而非 React 状态。
  • 使用 useRef 获取 DOM 节点,通过 .current.value 读取值。
  • 适合一次性读取(如提交时)、性能敏感或文件上传等场景。

示例:非受控评论框

import { useRef } from 'react';

export default function CommentBox() {
  const textareaRef = useRef(null);

  const handleSubmit = () => {
    const comment = textareaRef.current.value;
    if (!comment) return alert('请输入评论');
    console.log(comment);
  };

  return (
    <div>
      <textarea ref={textareaRef} placeholder="输入评论···" />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

这里没有使用 valueonChange,而是直接通过 ref 在提交时获取值,避免了不必要的状态更新。


三、受控 vs 非受控:如何选择?

场景推荐方式
实时校验(如密码强度、格式检查)✅ 受控组件
多字段联动(如省市区选择)✅ 受控组件
表单值需频繁用于 UI 更新✅ 受控组件
一次性提交(如简单留言)✅ 非受控组件
文件上传 <input type="file">✅ 非受控组件(必须)
性能敏感、避免频繁重渲染✅ 非受控组件

⚠️ 注意:<input type="file"> 是只读的,无法通过 value 设置,因此只能是非受控组件


四、useRef 的核心作用

useRef 是 React 提供的一个 Hook,用于创建一个可变且持久的引用对象,其 .current 属性可被读写,但不会触发组件重新渲染

useState 的对比:

特性useStateuseRef
是否触发重渲染✅ 是❌ 否
用途管理响应式状态存储可变值、引用 DOM
初始值useState(initial)useRef(initial)
更新方式setState(newValue)ref.current = newValue

useRef 的典型应用场景:

  1. 访问 DOM 元素

    const inputRef = useRef(null);
    useEffect(() => {
      inputRef.current.focus(); // 自动聚焦
    }, []);
    
  2. 存储定时器 ID(避免闭包问题)

    const intervalId = useRef(null);
    const start = () => {
      intervalId.current = setInterval(() => console.log('tick'), 1000);
    };
    const stop = () => {
      clearInterval(intervalId.current);
    };
    
  3. 保存上一次的状态(配合 useEffect)
    (常用于比较 prev/next 值)


五、混合使用:一个组件中同时存在受控与非受控

实际开发中,一个表单可能部分字段需要实时校验(受控),另一部分只需提交时读取(非受控)。React 允许混合使用:

import { useState, useRef } from 'react';

export default function App() {
  const [username, setUsername] = useState(''); // 受控
  const passwordRef = useRef(null); // 非受控

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('用户名:', username);
    console.log('密码:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="用户名(受控)"
      />
      <input
        type="password"
        ref={passwordRef}
        placeholder="密码(非受控)"
      />
      <button type="submit">登录</button>
    </form>
  );
}

💡 虽然可行,但建议保持一致性,避免逻辑混乱。


六、总结

  • 受控组件:状态驱动,适合复杂交互,代码更“React 式”。
  • 非受控组件:DOM 驱动,适合简单场景,性能更优。
  • useRef:不是状态管理工具,而是“默默奉献”的引用容器,用于 DOM 访问或存储可变值而不触发渲染。

在实际项目中,优先考虑受控组件,因为它更符合 React 的声明式理念;但在特定场景下(如文件上传、性能优化),非受控组件是更合理的选择。

掌握两者的区别与适用场景,是写出高效、可维护 React 表单的关键一步。


📌 最佳实践建议
对于大多数业务表单(登录、注册、设置等),使用受控组件 + 表单验证库(如 Formik、React Hook Form)是更稳健的方案。而像搜索框、评论框等轻量输入,可酌情使用非受控以简化逻辑。