React 表单的控制欲:什么时候我们真得控制它了,什么时候该放养了?

在写 React 代码时,你有没有过这样的困惑:

我只是想让用户输入一段文字,然后提交。

为什么非要搞个 useState 来管理它?

ref 直接读取 DOM 值不行吗?

如果你也这样想过,那你不是一个人。

今天我们就来聊一聊这个看似简单、实则深藏玄机的问题:React 中的受控组件和非受控组件,到底该选谁?


一、问题的起点:我该如何获取表单值?

最原始的需求是:用户输入内容 → 点击提交 → 获取输入内容

这听起来很简单,但实现方式却有两条路:

  • 方案A :给 <input> 加一个 value 属性,并通过 onChange 更新状态。
  • 方案B :不设置 value,只用 ref 在提交时读取 DOM 的 .value

我们先看一段代码:

ini 复制代码
const [value, setValue] = useState('');
const inputRef = useRef(null);

const doLogin = (e) => {
  e.preventDefault();
  console.log(inputRef.current.value); // 从 DOM 读值
};

return (
  <form onSubmit={doLogin}>
    <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
    <input type="text" ref={inputRef} />
    <button type="submit">登录</button>
  </form>
);

这段代码里有两个输入框:

  • 第一个是受控的(value={value}
  • 第二个是非受控的(ref={inputRef}

当你点击"登录",你会看到两个值都被打印出来 ------ 但它们的来源完全不同。


二、思考:为什么需要"控制"?

我们先问自己一个问题:

如果我不用 state 控制输入框,而是直接读 DOM,是不是更省事?

看起来是的。代码少,逻辑清晰,还能避免状态同步的问题。

但这里有一个关键的认知偏差:我们以为"读取值"只需要一次操作,但实际上,用户的交互是一个持续的过程。

🌪️ 想象这样一个场景:

你在做一个登录表单,要求密码至少 6 位,且包含数字。

如果使用非受控组件,你只能在提交时判断是否符合规则。

但如果用户输入了 abc123,系统提示"太短了",然后他继续打 456,变成 abc123456,这时候你才发现合格。

问题是:你怎么知道他在中间改了多少次?怎么实时反馈?

而受控组件可以做到这一点:

ini 复制代码
<input
  type="password"
  value={form.password}
  onChange={(e) => {
    const pwd = e.target.value;
    setForm({ ...form, password: pwd });
    if (pwd.length >= 6 && /\d/.test(pwd)) {
      setValid(true);
    } else {
      setValid(false);
    }
  }}
/>

这就是"控制"的价值:把数据流从"被动响应"变成"主动驱动"。


三、再深入一步:什么是"受控"?

很多人误以为"受控"就是"加了个 value",其实不然。

真正的"受控"是一种设计理念

所有的 UI 状态都由 React 的 state 驱动,而不是由 DOM 自行决定。

这意味着:

  • 输入框的值来自 state
  • 用户输入触发事件,更新 state
  • 页面重新渲染,显示新的值

这是一个闭环,形成了单向数据流

这种模式的好处在于:

  • 数据可预测(不会出现"页面显示 A,实际是 B"的问题)
  • 可以在任意时刻进行校验、重置、保存
  • 更容易测试和调试

ref 虽然能拿到值,但它绕过了 React 的状态系统,属于"黑箱操作"。


四、那什么时候该"放手"?

既然受控这么好,为什么还要有非受控组件?

因为有些场景,我们并不需要"控制"。

比如评论框:

ini 复制代码
const textareaRef = useRef(null);

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

在这个例子中:

  • 用户输入完就提交
  • 不需要实时校验
  • 不需要联动其他字段
  • 也不需要预览或自动补全

这时候,用 ref 是一种轻量级的选择

而且,在某些性能敏感的场景下,频繁触发 setState 会影响性能。例如文件上传、富文本编辑器等,这些组件内部有自己的状态管理机制,强行用 React 控制反而会增加复杂度。


五、结论:不是选择题,而是权衡题

回到最初的问题:我应该用受控还是非受控?

答案不是"哪个更好",而是:

根据业务需求做权衡。

场景 推荐方式 理由
登录/注册 受控组件 需要校验、联动、错误提示
评论/留言 非受控组件 一次性提交,无需实时处理
文件上传 非受控组件 DOM 内部状态复杂,不适合 React 管理
实时搜索 受控组件 需要即时反馈结果
富文本编辑器 非受控组件 使用第三方库,内部状态独立

六、最后的思考:React 的本质是什么?

React 的核心思想是:UI 是状态的函数

也就是说,页面长什么样,完全取决于当前的状态。

当你使用受控组件时,你是在践行这一理念:每一个变化,都是状态驱动的结果

而当你使用非受控组件时,你实际上是在说:"这个部分我暂时不想管,让它自己玩。"

这不是坏事,但你要清楚地知道:你在放弃一部分控制权。

所以,不要为了"简洁"而滥用非受控组件,也不要为了"规范"而过度使用受控组件。

我们应该要在"控制"与"放手"之间找到平衡点。


写在最后

技术没有绝对的对错,只有合适的时机。

下次面对一个表单时,可以先想想

"我需要在用户输入的过程中做什么?"

如果答案是"什么也不做",那就放手吧。(useRef)

如果答案是"我要校验、联动、展示",那就牢牢抓住它。(useState)

这才是 React 表单设计的真正智慧。

相关推荐
不想秃头的程序员2 小时前
吃透 JS 事件委托:从原理到实战,解锁高性能事件处理方案
前端·面试
AntoineGriezmann2 小时前
前端 Token 刷新机制实战:基于 Axios 的 accessToken 自动续期方案
前端
烟袅2 小时前
从定时器管理出发,彻底搞懂防抖与节流的实现逻辑
前端·javascript
UIUV3 小时前
JavaScript 遍历方法详解
前端·javascript·代码规范
Zyx20074 小时前
React 中的 Props:组件通信与复用的核心机制
前端
海云前端14 小时前
大模型Function Calling的函数如何调用的?
前端
ohyeah4 小时前
防抖与节流:前端性能优化的两大利器
前端·javascript
Zyx20074 小时前
React Hooks:函数组件的状态与副作用管理艺术
前端