Menu

React 受控组件与非受控组件的区别

受控输入框的值来自 React 状态,只能通过 onChange 改变。非受控输入框把值保存在 DOM 里,你用 ref 或 FormData 读取它。了解两者的区别、常见警告,以及各自适用的场景。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

受控组件是值保存在 React 状态里的表单元素:你传入 value,在 onChange 中更新状态,由 React 决定输入框显示什么。非受控组件则把值保存在 DOM 里,和普通 HTML 一样:你传入初始值 defaultValue,之后再通过 ref 或 FormData 读取当前值。

在两个输入框里分别输入文字。段落会随第一个输入框的每次按键变化,因为每次改动都会设置状态并重新渲染。第二个输入框只在 DOM 中变化;React 不会重新渲染,只有在“Read both”去读取时你才能看到它的值。

受控输入框

使用受控输入框时,React 状态是唯一的数据来源。每次按键的流程是:

  1. 用户输入,浏览器触发一个 input 事件。
  2. 你的 onChange 处理函数读取 e.target.value,然后调用 setter。
  3. React 重新渲染,把新的状态写回输入框的 value。

因为你的代码处在这个循环的中间,所以能做到非受控输入框做不到的事:拒绝或改写字符,把值显示在别处,在值有效之前禁用按钮,或者在代码中设置值(一个“清空”按钮,一个用户点击的建议)。

试着输入字母:它们永远不会出现,因为处理函数在设置状态之前就把所有非数字字符去掉了。在输入正好六位数字之前,Verify 按钮一直是禁用的。

表单一页的示例用的都是受控写法,在大多数 React 代码中,它也是默认的选择。

非受控输入框

非受控输入框的工作方式和普通 HTML 一样。React 用 defaultValue(复选框和单选框用 defaultChecked)渲染它一次,之后就不再管它。要读取值,可以保存一个指向该元素的 ref,或者在提交时用 FormData 读取表单中所有带名字的字段。

字段没有对应的状态,也没有 onChange。只有在表单提交时才会收集这些值。取消勾选复选框再发送一次:subscribe 键消失了,因为未勾选的复选框根本不会出现在 FormData 中。

首次渲染之后再修改 defaultValue,对输入框没有任何作用,因为 React 只在创建元素时应用它。要重置非受控表单,可以调用 e.target.reset(),或者给表单一个新的 key,让 React 创建全新的输入框。

无法输入的输入框

如果你传了 value 却没有传 onChange,输入框就是受控的,但没有任何东西去更新状态。每次按键后 React 都会把同样的值写回去,于是输入框看起来像被冻住了。

试着在每个字段里输入。第一个不会变化。第二个是受控输入框的正确修法,你在里面输入时,第一个和最后一个字段也会跟着变,因为三者读取的是同一个状态。第三个能输入,因为它是非受控的。第四个特意声明了它不可编辑。

在开发环境中,React 会为第一个输入框打印一条警告:“You provided a value prop to a form field without an onChange handler”。预览按生产版本运行,所以这里看不到这条警告,但在你自己的开发服务器上能看到。

从非受控变成受控

React 通过 value 判断输入框是否受控。如果 value 是 undefined 或 null,输入框就是非受控的。所以一个 value 起初是 undefined、后来变成字符串的输入框,会在生命周期中从非受控切换为受控,React 在开发环境中会警告:“A component is changing an uncontrolled input to be controlled”。

这个警告指向一个真实的 bug。反过来,从字符串变回 undefined,输入框又会变成非受控,React 也就不再往里写值。

在两个输入框里都输入一些文字,然后分别点击各自的 Clear 按钮。第二个输入框被清空了。第一个仍保留着文字:把状态设为 undefined 让它变成了非受控,React 不再控制它显示什么。

undefined 通常来自尚未加载的数据(在用户数据到达之前写 value={user.name}),或者还不存在的对象字段。有两种修法:

const [name, setName] = useState('');   // start with a string
<input value={user?.name ?? ''} />       // or fall back to one

复选框的 checked 也遵循同样的规则:初始值设为 false,而不是 undefined。

文件输入框总是非受控的

<input type="file"> 无法受控,因为浏览器不允许页面设置它的值(如果页面能随意选取你磁盘上的文件,那就是一个安全漏洞)。可以在 onChange 中从 e.target.files 读取选中的文件,或者在表单提交时通过 ref 读取。

从电脑上选择一两个文件;不会上传任何东西,示例只列出文件名和大小。文件输入框唯一允许设置的值是空字符串,Clear 按钮就是这样重置它的。

如何选择

受控非受控
值保存在React 状态DOM
设置初始值value / checkeddefaultValue / defaultChecked
读取值随时从状态中读取用 ref,或在提交时用 FormData
每次按键都重新渲染是否
在代码中修改值设置状态通过 ref 写入 DOM
适用于实时校验、格式化、相互依赖的字段提交时才读取的简单表单、文件输入框、表单 action

当界面需要在用户输入时对值做出反应,选受控。只在最后才需要这些值时,选非受控,React 19 的表单 action 也是这样工作的:<form action={fn}> 把一个 FormData 交给你的函数,并在之后重置非受控字段。

每次按键都重新渲染听起来开销很大,但对普通表单来说并不是问题。只有当一棵庞大而缓慢的组件树随输入框一起重新渲染时才会成为问题;这时把状态放在离输入框近的地方,或者把慢的部分移到单独的组件里。

这两个词同样可以描述你自己的组件。通过 props 从父组件拿到值和 onChange 的组件,由父组件控制;自己保存状态的组件则是非受控的。为了让组件受控而把状态往上移,正是状态提升要讲的内容。

常见问题

React 中的受控组件是什么?

受控组件是当前值来自 React 状态的表单元素,用 value(或 checked)设置,通过 onChange 更新。React 状态是唯一的数据来源,所以你随时都知道它的值,也能在代码里修改它。

React 中的非受控组件是什么?

非受控组件是把值保存在 DOM 里的表单元素,和普通 HTML 一样。你用 defaultValue(或 defaultChecked)给它一个初始值,在需要时再读取当前值,比如通过 ref,或在提交时通过 FormData。

为什么我的 React 输入框无法输入?

你传了 value 却没有传 onChange。这个输入框是受控的,所以每次按键后 React 都会把状态里的值写回去。可以加一个更新状态的 onChange,改用 defaultValue,或者在它确实不该改变时加上 readOnly。

怎样修复“A component is changing an uncontrolled input to be controlled”?

输入框的 value 一开始是 undefined(或 null),后来变成了字符串。确保它始终是字符串:把状态初始值设为 '',或写成 value={name ?? ''}。

应该用受控输入框还是非受控输入框?

当界面需要在用户输入时对值做出反应,就用受控输入框:校验、格式化、禁用按钮、相互依赖的字段。只在提交时需要这些值时,用非受控输入框;文件输入框也一样,它总是非受控的。

Coddy 编程语言插图

和 Coddy 一起学编程

开始