受控组件与非受控组件详解(React 视角)

受控组件与非受控组件详解(React 视角)

“受控组件(Controlled Component)/ 非受控组件(Uncontrolled Component)”是 React 表单开发中的基础概念,也是面试高频题。理解它们的区别,能帮助你在表单、输入框、富文本、文件上传等场景做出正确选型,并避免状态错乱、性能抖动等问题。

本文以 React 为主(因为“受控/非受控”在 React 语境中最典型),同时也会补充一些通用思路。


一、先用一句话理解

  • 受控组件:表单值由 React state 驱动,value 由 state 提供,onChange 更新 state。
  • 非受控组件:表单值由 DOM 自己维护,React 不用 state 实时保存值,需要时通过 ref 读取。

二、受控组件(Controlled)

1. 基本写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from "react";

export default function ControlledInput() {
const [value, setValue] = useState("");

return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="请输入..."
/>
<p>当前值:{value}</p>
</div>
);
}

关键点:

  • value 永远来自 state
  • 输入框变化时触发 onChange,再更新 state
  • state 更新后重新渲染,value 变更回写到 input

2. 受控组件的优点

  • 单一数据源(Single Source of Truth):值始终在 React state 中,逻辑更可控。
  • 易于做校验与格式化:例如输入时限制数字、自动 trim、实时校验提示。
  • 易于联动:多个字段互相影响(比如省市区联动、禁用/显示条件)更自然。
  • 提交更简单:提交时直接使用 state,无需去 DOM 取值。

3. 受控组件的常见坑

(1)忘记写 onChange 导致输入框不可编辑

1
<input value={value} />

React 会认为这是“只读输入”,用户输入不会生效(控制权在你手里,你却没更新)。

(2)性能:大量字段/高频输入导致频繁 re-render

受控组件每次输入都会 setState → re-render。对简单表单通常没问题,但在以下场景需要注意:

  • 超长列表中的输入框(如表格编辑)
  • 富文本/高频输入的复杂组件
  • 渲染成本较高的父组件树

优化思路:

  • 将 state 下沉到更小的组件,减少影响范围
  • 使用 React.memo、拆分组件
  • 对昂贵计算使用 useMemo
  • 对“提交时才需要值”的字段考虑非受控或混合策略

(3)受控与非受控切换警告

当一个 input 一开始是 undefined(非受控),后面变成字符串(受控),会出现警告:

A component is changing an uncontrolled input to be controlled…

解决方式:

  • 初始化 state 为 "" 而不是 undefined/null
1
const [value, setValue] = useState("");

三、非受控组件(Uncontrolled)

1. 基本写法(ref 读取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useRef } from "react";

export default function UncontrolledInput() {
const inputRef = useRef(null);

const handleSubmit = () => {
const value = inputRef.current?.value;
console.log("提交值:", value);
};

return (
<div>
<input ref={inputRef} defaultValue="" placeholder="请输入..." />
<button onClick={handleSubmit}>提交</button>
</div>
);
}

关键点:

  • 使用 defaultValue(初始化值),而不是 value
  • 之后输入框内容由 DOM 自己维护
  • 需要值时,通过 ref.current.value 读取

2. 非受控组件的优点

  • 更少的渲染:输入过程不必每次 setState,减少 re-render。
  • 更贴近原生表单:尤其适合“最终提交时读取一次值”的简单场景。
  • 适配第三方组件:有些第三方输入类组件更容易以非受控方式集成(视具体库而定)。

3. 非受控组件的缺点

  • 不利于实时校验与联动:值不在 state 中,做联动需要额外监听或手动读取。
  • 数据流分散:值在 DOM,其他业务状态在 React,容易出现“两个世界”的同步问题。
  • 更难做统一表单管理:例如“统一收集所有字段值、批量校验、统一重置”等。

四、受控 vs 非受控:对比与选型建议

维度 受控组件 非受控组件
数据源 React state DOM 自身
更新时机 每次输入都更新 state 需要时用 ref 读取
校验/格式化 很方便(实时) 需要额外处理
联动逻辑 更自然 更麻烦
性能 大量输入可能带来 re-render 通常更轻
工程可控性 更强 更接近原生、但可控性弱

选型建议(实战)

  • 业务表单(登录/注册/编辑页):优先受控(利于校验、联动、提交)。
  • 极简单表单、只需提交读取一次:可以用非受控(减少样板代码)。
  • 文件上传 <input type="file">:更常见是非受控(浏览器安全限制,value 不可随意设置)。
  • 富文本编辑器/复杂输入控件:常见是“混合策略”(内部非受控,外层在某些时机同步到 state)。

五、进阶:混合策略(表单里很常见)

很多真实项目不是“纯受控/纯非受控”,而是混合:

  • 输入过程中非受控,避免高频 re-render
  • 失焦(onBlur)或提交时把值同步到 state / 表单模型

示例(onBlur 同步):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useRef, useState } from "react";

export default function HybridInput() {
const inputRef = useRef(null);
const [value, setValue] = useState("");

return (
<div>
<input
ref={inputRef}
defaultValue={value}
onBlur={() => setValue(inputRef.current.value)}
/>
<p>同步到 state 的值:{value}</p>
</div>
);
}

这种策略适合:

  • 输入非常频繁,但不需要实时联动
  • 需要在某些关键节点(失焦/提交)进入 React 数据流

六、如何“重置”表单?

1. 受控组件重置

直接重置 state:

1
setValue("");

2. 非受控组件重置

有两种常见方式:

  • 手动改 DOM:
1
inputRef.current.value = "";
  • 或利用 key 让组件重新挂载(重置 defaultValue/内部状态):
1
2
3
4
5
6
7
const [resetKey, setResetKey] = useState(0);
return (
<>
<input key={resetKey} defaultValue="" />
<button onClick={() => setResetKey((k) => k + 1)}>重置</button>
</>
);

七、总结

  • 受控组件:数据在 React state 中,适合绝大多数业务表单(校验、联动、提交清晰)。
  • 非受控组件:数据在 DOM 中,适合简单场景或性能敏感输入(提交时读取、少渲染)。
  • 混合策略:在复杂表单/高频输入场景很常见(输入不频繁同步,关键节点再同步)。

理解受控/非受控的本质后,你在选择表单方案时就不会只停留在“背定义”,而是能根据业务复杂度、性能与可维护性做出合理决策。


受控组件与非受控组件详解(React 视角)
https://sunjc.vip/2025/06/08/受控组件与非受控组件详解/
作者
Sunjc
发布于
2025年6月8日
许可协议