先记住这个答案
文本输入框一开始传入 undefined 或 null 作为 value,后来异步数据变成字符串,容易发生非受控到受控的切换。受控文本输入应始终得到字符串并在 onChange 同步更新状态;复选框则始终提供布尔 checked。defaultValue 和 defaultChecked 用于非受控初始值,不能用来维持后续受控同步。修复重点是统一数据模型,避免让同一个 DOM 控件在两种管理方式之间切换。
- 文本 value 从第一次渲染起就保持字符串
- 复选框受控字段是布尔 checked
- defaultValue 不负责同步后续接口回填
异步回填为什么最容易触发这个问题
假设编辑资料页面把接口返回的 profile.name 直接传给 value,初始 profile 为空,第一次得到 undefined。请求完成后 name 变为字符串,控件管理方式随之改变。可以把表单初始状态建模为空字符串,或在渲染边界使用空值合并提供字符串兜底,同时保留加载与错误状态。
不要把所有字段都简单写成 value || 空字符串。这个逻辑会吞掉数字零等合法假值,空值合并更能表达只处理 null 与 undefined 的意图。数字输入在编辑过程中还可能处于空串、负号等暂态,要明确保存输入文本与提交时数值转换的边界。
区分文本框、复选框和默认值
文本框由 value 控制显示内容,复选框的勾选状态由 checked 控制。给 checkbox 传 value 只是设置它被提交时的值,并不等价于控制是否勾选。状态为可选值时应转成明确的布尔初始值,事件中读取 target.checked,而不是把 target.value 当作真假。
非受控控件则由 DOM 保存当前输入,defaultValue 主要提供初始值。接口回填需要主动改变当前展示时,要么选择受控方案,要么明确重建控件或使用合适的表单工具;仅改变 defaultValue 不能被当成持续同步用户输入的方式,也不应同时混用两套参数。
封装表单组件时明确数据契约
公共 Input 组件应声明自己接收的值类型和空值约定。例如文本控件对外统一字符串,数据层可缺失字段在进入控件前转换;不要让内部某个分支传 value,另一个分支只传 defaultValue。这样业务端即便切换加载状态,控件本身也保持稳定模式。
排查时记录的是控件首次挂载到后续更新的值,而不只是报错时最后看到的值。还需检查是否给了受控值却没有有效的 onChange,使控件无法编辑。通过不断卸载重建消除告警,会同时丢失焦点和未保存输入,应避免把生命周期副作用当成数据模型修复。
容易答错的地方
- 认为给 undefined 加类型断言就能修复
- TypeScript 断言不改变运行时传入值。若实际仍是 undefined,React 看到的控件模式也不会改变;应在状态初始化或组件边界真实地转换空值。
- 把 checkbox 的 value 当成勾选状态
- checkbox 的 value 与 checked 分别表达提交值和选择状态。只修改前者不会可靠地控制视觉勾选,事件处理也需要读取正确字段,不能直接沿用文本输入框的处理器。
面试官还会怎么问?
只读受控输入框也一定要 onChange 吗?
如果业务明确不允许编辑,可以使用适当的只读设置表达意图;可编辑受控控件则需要同步状态的事件处理。不要让看起来可以编辑的控件因为状态从不更新而不断恢复旧内容。
接口返回 null 的文本字段如何处理?
在进入文本控件之前,把“尚无文本”的 null 转为空字符串,并把“加载中”放在独立状态中。这样空文本与请求状态不会挤在同一个 value 类型里。
更换 key 后警告消失,是否说明修好了?
只能说明旧控件生命周期被终止,新控件重新初始化。若业务没有新的组件身份,这样处理可能掩盖初始值错误并造成焦点丢失;仍需核对同一实例是否维持一致的受控模式。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。