Base UI Field.Error 外部受控时红框有、报错文字消失的根因
用 @base-ui-components/react 封装设计系统的 Field 组件,逻辑很简单:父级传 error 字符串进来,就把 Field.Root 标记为 invalid,再把字符串塞进 <Field.Error> 显示。
样式层完全正常——输入框红框红环如期出现。但错误文字呢?DOM 里根本找不到那个节点。控制台没有警告,组件没有报错,一片安静。最迷惑的症状:视觉上"看起来有反馈"(有红色),实际上无障碍和视觉文案都是哑的。
现象
// 封装层
<Field.Root invalid={Boolean(error)}>
<Field.Control render={<input />} />
<Field.Error>{error}</Field.Error> {/* 从不渲染 */}
</Field.Root>- 输入框:
data-invalid属性挂上了,红框出现了。 <Field.Error>对应的 DOM 节点:消失,aria-describedby无内容。- 去掉
errorprop,红框消失——说明invalid传递路径是通的,唯独Field.Error不响应。
根因
读 Base UI 源码 node_modules/@base-ui-components/react/esm/field/error/FieldError.js,渲染决策只有这几行:
let rendered = false;
if (formError || match === true) { rendered = true; } // 强制渲染
else if (match) { rendered = Boolean(validityData.state[match]); }
else { rendered = validityData.state.valid === false; } // 默认分支
if (!rendered) return null;默认分支检查的是 validityData.state.valid。这个值只有在 Base UI 自己跑完 validate / validationMode 后才会从 null 变成 false。
我们完全绕过了 Base UI 的验证机制——valid 永远是 null,null === false 为 false,FieldError 每次都 return null。
红框和报错文字走的是两条互不关联的路径:
- 红框:
Field.Root invalid={true}→ 根 contextstate.valid = false→ 控件写入data-invalid。 - 报错文字:
validityData.state.valid(Base UI 内部验证结果)→ 从未被触发 → 永远null。
invalid={true} 让边框变红,但 Field.Error 的渲染条件是独立的 validityData。两者解耦,各走各路。外部控制错误字符串时,validityData 永远停在默认值 valid: null,报错文字因此静默消失。
正解
传 match={true} 绕过 validityData 检查,强制渲染;用条件渲染在 error 为空时不挂节点:
{error && (
<Field.Error match={true} className="text-sm text-danger">
{error}
</Field.Error>
)}match === true 命中第一个分支,rendered = true,无论 validityData 状态如何都会渲染。
更完整的封装:
interface FieldProps {
label?: string;
error?: string;
invalid?: boolean;
children: React.ReactNode;
}
export function Field({ label, error, invalid, children }: FieldProps) {
const isInvalid = invalid ?? Boolean(error); // error 非空时自动 invalid
return (
<BaseField.Root invalid={isInvalid}>
{label && <BaseField.Label>{label}</BaseField.Label>}
{children}
{error && (
<BaseField.Error match={true} className="text-sm text-danger">
{error}
</BaseField.Error>
)}
</BaseField.Root>
);
}match={true} 不只是绕过验证检查——FieldError 内部仍然调用 setMessageIds,会把错误节点的 id 自动写入控件的 aria-describedby。比自己手写一个 <p> 少很多胶水代码,无障碍免费获得。
排查提示
如果在别的外部受控场景遇到「样式有、内容无」的 Base UI 组件,先去源码找 rendered 或类似的条件决策,再对照:组件依赖的 context 值是否需要 Base UI 内置验证才能写入?如果是,就找对应的 match 参数或强制渲染 flag。
一句话外卖
Field.Root invalid只管样式,Field.Error靠的是validityData——外部控制错误时两者脱节,加match={true}强制渲染才能让文字和红框同时出现。