Base UI rc.0 的 Select 没有 placeholder prop——用 null 项注入绕坑
用 @base-ui-components/react 做一个下拉选择器,照着 context7 / 官方文档写 <Select.Value placeholder="请选择…" />,TypeScript 直接报红:Property 'placeholder' does not exist on type SelectValueProps。用 as any 绕过之后,trigger 仍然空白——placeholder 文本根本不显示。单测倒是绿的,但那是假绿。
现象
tsc报SelectValueProps上没有placeholder属性,只有children。- 强行用类型断言传入后,渲染结果是空白 trigger,用户看不到占位文本。
- 单测里断言
span.getAttribute("placeholder")通过——attribute 确实被挂上去了,但 可见文本依然为空(假绿)。
根因
@base-ui-components/react 1.0.0-rc.0 的 Select.Value 只有 children prop,placeholder 是 v1.2+ 才新增的。
context7 和官方文档返回的示例基于最新稳定版,而项目 lockfile 锁的是 rc.0——这是两套完全不同的 API,文档里的 placeholder 在 rc.0 根本不存在。
rc.0 的 SelectValueProps:
{
children?: React.ReactNode | ((value: any) => React.ReactNode);
}传入 placeholder 只是给 DOM 挂了一个无意义 attribute,不会被渲染成文本。单测只验 attribute 就会假绿,必须验 textContent。
排查:probe 真实版本的 .d.ts
别靠文档或记忆,直接读安装版本的类型声明:
SEL=$(node -e "console.log(require.resolve('@base-ui-components/react/select',{paths:['packages/ui']}))")
cat "$(dirname "$SEL")/value/SelectValue.d.ts"rc.0 只有 children,无 placeholder。再读 SelectValue.js 的渲染逻辑,children 有三个分支:
- 函数
childrenProp(value)→ 自己控制渲染(注意收到的是 raw value,不是 label)。 - 非空非函数 children → 恒显示该节点,选了值也不会换——所以不能直接写字符串当 placeholder。
- 不写 children → 自动调用
resolveSelectedLabel(value, items):有值时映射 items 里对应 label,无值时命中items.find(it => it.value == null)并显示其 label。
这就是正解的切入点。
正解
在 items 前插入一个 value: null 的哑项作为 placeholder,Select.Value 不写 children,让它自动走第三个分支:
// 包装一层,接受 placeholder prop,内部转成 null 项注入
export function Select({ items, placeholder, children, ...props }) {
const finalItems =
placeholder != null && items != null
? [{ value: null, label: placeholder }, ...items]
: items;
return (
<BaseSelect.Root items={finalItems} {...props}>
{children}
</BaseSelect.Root>
);
}
// Trigger 内的 Value 不写 children
// 无值时自动显示 null 项的 label(即 placeholder)
// 有值时显示对应 label
<BaseSelect.Value className="truncate data-[placeholder]:text-muted" />列表里只渲染真实选项(map 原始 items,不含 null 项),null 项因为不在列表里也无法被用户选中,完全符合 placeholder 语义。
| 写法 | 无值时 | 有值时 |
|------|--------|--------|
| <Value/> + items 无 null 项 | 空白 | 显示 label ✅ |
| <Value placeholder="X"/> (rc.0) | 空白(prop 无效) | — |
| <Value/> + items 含 {value:null,label:"X"} | 显示 "X" ✅ | 显示 label ✅ |
同时修正测试写法,改验 textContent 而非 attribute:
// ✅ 验可见文本
expect(screen.getByRole("combobox").textContent).toContain("请选择");
// ❌ 别只验 attribute(attribute 在但文本不渲染 = 假绿)
// expect(span.getAttribute("placeholder")).toBe("请选择");一句话外卖
context7 / 官方文档给的是最新版 API,你的 lockfile 锁的是 rc.0——凡是 rc/beta 依赖,API 用法必须 probe 安装版本的真实
.d.ts+ 跑真实渲染验证,不能信文档、不能信 AI 记忆、不能只验 attribute。