DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

React 输入框从 uncontrolled 变成 controlled

React 文本 input 首次 value 为 undefined,回填后切换成受控。确定初始值、清空及 reset 契约,区分原生 input 与表单库包装组件。

表单第一次显示正常,输入或接口回填后出现 A component is changing an uncontrolled input to be controlled。对原生文本 input,常见路径是初次传入 value={undefined},随后变成字符串;React 看到了同一个输入框在两种控制方式之间切换。

本文按 React 19.1.1 的原生 input 契约说明。表单库和 UI 组件可能还有自己的默认值、清空值和 reset 规则,不能把原生 input 的处理直接当作所有组件的通用默认值。

决定当前值由谁保存

受控输入用 value 指定当前值,并在 onChange 中同步更新 state。非受控输入用 defaultValue 提供初始值,后续由 DOM 保存当前内容。不能同时用这两个属性管理同一个文本输入,也不能把 defaultValue 当成每次接口回填都会生效的当前值。

已有受控表单时,从空字符串开始就足够:

import { useState } from 'react';

export default function NameField() {
  const [name, setName] = useState('');

  return (
    <label>
      姓名
      <input
        name="name"
        value={name}
        onChange={event => setName(event.target.value)}
      />
    </label>
  );
}

接口还没返回时,state 也维持字符串。数据边界若确实允许缺失,可在传给 input 前用 value={name ?? ''} 归一化。这个写法只处理 null 和 undefined;它不负责把任意对象转换成合理的输入文本,也不应悄悄掩盖接口字段类型错误。

复选框对应的是 checked 和布尔值,例如初始为 false,读取 event.target.checked。不要为了统一写法给所有字段都套 value || '':数字零、布尔假和空字符串可以是有意的值。

回填和重置也要保持同一种控制方式

检查首次渲染、加载完成、用户编辑、提交后 reset 四个阶段。某个阶段把字段删掉或重置成 undefined,仍会产生反方向的 controlled 到 uncontrolled 警告。受控文本字段清空为 '',而不是删除它的值。

使用 Controller 一类包装组件时,还要核对它实际传给 input 的 value,以及初始值由表单还是单个字段提供。旧版本的默认值覆盖行为不能当成当前库的保证;应按项目正在使用的版本设置,避免两处各维护一份初始值。

若改成非受控表单,移除 value,用 defaultValue 初始化,并在提交时读取表单值。异步数据晚到时,是否重置表单、保留用户已输入的内容,是另一个需要明确的产品行为。不要只靠不断改变 key 强制重挂载来压掉警告,它会重建 DOM 并可能丢掉输入和焦点。

修正后逐一检查加载、编辑与重置:控制台不再报告模式切换,输入可正常修改,回填或清空后的可见值与提交值一致。没有 onChange 导致的只读警告是另一条契约,不能用空字符串初始化替代它的处理。