ant-design Checkbox 全选(check-all)实战:indeterminate 半选状态原理与实现详解
在实现「全选 / 反选」这类交互时,Ant Design Checkbox 组件提供的 indeterminate(半选 / 未完全选中)属性是核心工具:它能让主复选框在「全选、部分选中、全不选」三种视觉状态下精确反馈子项选择进度。本指南以 components/checkbox/demo/check-all.tsx 演示为主线,结合 Checkbox 源码 与官方文档 index.zh-CN.md,完整讲解从 demo 代码复用到源码级原理的实现路径。读完你可以独立搭建健壮的全选列表,并理解 indeterminate 背后「只负责样式控制」的真实含义。
一、check-all 演示:一张图看懂全选三态模型
Ant Design 官方仓库中专门提供了 check-all 演示(中文说明),它的说明只有一句话,却点出了全选实现的关键:
在实现全选效果时,你可能会用到
indeterminate属性。
其配套的完整代码位于 components/checkbox/demo/check-all.tsx。全选场景下的复选框其实要表达三种状态:
| 主复选框状态 | 触发条件 | 用户感知 |
|---|---|---|
checked(全选) |
子项全部被选中 | 勾选框内显示完整对勾 |
indeterminate(半选) |
子项部分被选中(0 < 已选 < 总数) | 勾选框内显示短横线 - |
| 未选中 | 子项全部未选 | 空框 |
演示代码用两个派生布尔值精确刻画了「部分选中」与「全选」两种边界,这是整个全选交互的逻辑核心:
const checkAll = plainOptions.length === checkedList.length; // 全选判定
const indeterminate =
checkedList.length > 0 && checkedList.length < plainOptions.length; // 半选判定
需要注意判定顺序:indeterminate 在代码层面先于 checkAll 求值,但当所有项都选中时,checkedList.length === plainOptions.length 成立,此时 checked 主复选框的完整勾选优先于半选样式呈现——主复选框最终渲染为「全部选中」而非「部分选中」。这正是全选 UI 需要同时绑定 checked 与 indeterminate 两个属性的原因。
二、demo 复刻:受控主复选框 + 子 Checkbox.Group 的双向数据流
官方 check-all.tsx 采用「单一数据源」模式:一个 checkedList 状态数组同时驱动主复选框与子选项组。下面是演示代码的完整实现,它是可直接复制运行的入门模板:
import React, { useState } from 'react';
import { Checkbox, Divider } from 'antd';
import type { CheckboxProps } from 'antd';
const CheckboxGroup = Checkbox.Group;
const plainOptions = ['Apple', 'Pear', 'Orange'];
const defaultCheckedList = ['Apple', 'Orange'];
const App: React.FC = () => {
const [checkedList, setCheckedList] = useState<string[]>(defaultCheckedList);
const checkAll = plainOptions.length === checkedList.length;
const indeterminate = checkedList.length > 0 && checkedList.length < plainOptions.length;
const onChange = (list: string[]) => {
setCheckedList(list);
};
const onCheckAllChange: CheckboxProps['onChange'] = (e) => {
setCheckedList(e.target.checked ? plainOptions : []);
};
return (
<>
<Checkbox indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
Check all
</Checkbox>
<Divider />
<CheckboxGroup options={plainOptions} value={checkedList} onChange={onChange} />
</>
);
};
export default App;
这份代码揭示了全选功能需要维护的两条数据流:
- 子项 → 主复选框(单向派生):子项组
CheckboxGroup的任何勾选变化,都会通过其onChange(list)回调更新checkedList。主复选框不持有独立状态,它的checked与indeterminate完全由checkedList与总选项数实时推导,保证主复选框永远忠实反映子项选择结果。 - 主复选框 → 子项(一次性批量设置):点击主复选框时,
onCheckAllChange通过e.target.checked判定用户意图——勾选则把子项checkedList整体置为全部选项plainOptions,取消勾选则置为空数组[]。子项组的value属性因此同步刷新。
关键细节:子项组使用受控形式 value={checkedList}(配合 onChange),主复选框同样显式传入 checked,两者构成一个闭环受控结构,避免了「半受控」导致的状态不同步问题。官方 API 文档 index.zh-CN.md 也确认了该使用姿势:checked 用于「指定当前是否选中」,indeterminate 则负责表达部分选中。
提示:
Checkbox.Group也可以通过children形式组合若干<Checkbox value="...">子节点实现同一效果;options数组写法只是更方便声明式维护。两种形式下 Group 的onChange回调签名均为(checkedValue: T[]) => void,详见 Group.tsx。
三、indeterminate 源码级原理:为何它「只负责样式控制」
官方 API 文档 index.zh-CN.md 对 indeterminate 的完整定义为:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| indeterminate | 设置 indeterminate 状态,只负责样式控制 | boolean | false |
注意表格中明确写着「只负责样式控制」——这在语义上极其重要:HTML 原生 checkbox 的 indeterminate 属性是一个不受用户交互直接触发的只读视觉状态,用户无法通过点击进入或退出半选;半选是应用层根据业务数据(如已选数量)计算后赋值的。
源码 Checkbox.tsx 中有两处证据印证了这一点。首先,组件接收 indeterminate 并默认置为 false:
const {
...
indeterminate = false,
...
} = props;
随后在一个 useEffect 中把它同步到底层原生 input 元素的 indeterminate DOM 属性上:
// ========================== Indeterminate ===========================
React.useEffect(() => {
if (checkboxRef.current?.input) {
checkboxRef.current.input.indeterminate = indeterminate;
}
}, [indeterminate]);
这里 indeterminate 被同步到了 DOM 节点的属性而非 React 的渲染属性,正是因为原生 checkbox 的 indeterminate 无法用标准 HTML 属性表达,只能通过 input.indeterminate = true 直接操作 DOM property 实现。与此同时,组件还会据此拼接样式类名:
const checkboxClass = clsx(
mergedClassNames.icon,
{ [`${prefixCls}-indeterminate`]: indeterminate }, // 渲染为 ant-checkbox-indeterminate
TARGET_CLS,
hashId,
);
CSS 通过这个 .ant-checkbox-indeterminate 类把原本的对勾替换为横线图标,从而完成「半选」的视觉呈现。测试用例 checkbox.test.tsx 对上述行为做了精确断言:
it('should reflect indeterminate state correctly', () => {
const { rerender, container } = render(<Checkbox indeterminate />);
const checkboxInput = container.querySelector('input')!;
expect(checkboxInput.indeterminate).toBe(true); // 首次渲染后原生属性为 true
rerender(<Checkbox indeterminate={false} />);
expect(checkboxInput.indeterminate).toBe(false); // 属性变化后同步为 false
});
该测试直接验证了「传入 indeterminate 属性 → 原生 input 的 indeterminate property 被同步」这条调用链,也解释了为什么全选必须由应用层状态派生而非让用户直接触发半选。
四、工程化进阶:Options 受控组合、禁用项与更多业务形态
官方演示为了可读性使用了静态字符串数组 plainOptions。真实业务中选项往往携带更多元信息或包含禁用项,此时可以把状态推导做得更健壮。
4.1 使用对象型 Options
当选项包含 disabled、label 与 value 分离时,需要基于 value 进行全选判定。可参考 Group.tsx 中 CheckboxOptionType 的定义(label、value、disabled、style、className、title 等字段):
import React, { useState } from 'react';
import { Checkbox, Divider } from 'antd';
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Pear', value: 'pear', disabled: true }, // 禁用项不可勾选
{ label: 'Orange', value: 'orange' },
];
const App: React.FC = () => {
const [checkedList, setCheckedList] = useState<string[]>(['apple']);
const selectableValues = options.map((o) => o.value); // 全选项 = 全部 value 集合
const allChecked = selectableValues.every((v) => checkedList.includes(v));
const indeterminate = checkedList.length > 0 && !allChecked;
const onCheckAllChange = (e: { target: { checked: boolean } }) => {
setCheckedList(e.target.checked ? selectableValues : []);
};
return (
<>
<Checkbox checked={allChecked} indeterminate={indeterminate} onChange={onCheckAllChange}>
全选
</Checkbox>
<Divider />
<CheckboxGroup options={options} value={checkedList} onChange={setCheckedList} />
</>
);
};
export default App;
这里用 every 代替 length 相等判断的好处是:即使未来增加选项或选项顺序变化,语义依然准确;disabled 选项天然不会被用户勾选,Group 的勾选回调也只会包含用户可交互勾中的值。
4.2 受控与非受控的选择
官方 API 文档 index.zh-CN.md 显示 Checkbox.Group 同时提供 value(指定选中的选项)与 defaultValue(默认选中的选项)两组属性。全选场景推荐显式受控(value + onChange),因为主复选框的 checked/indeterminate 需要基于实时数据派生;若使用非受控 defaultValue,你只能依赖 Group 的 onChange 回调在事件侧维护一份外部镜像数组,否则主复选框无法感知内部状态变化。从 Group.tsx 源码可以看到,Group 内部通过 useState 保存值,并仅在外部传入 value 时以 effect 同步外部值:
React.useEffect(() => {
if ('value' in restProps) {
setValue(restProps.value || []);
}
}, [restProps.value]);
这从实现层面印证了:一旦传入受控 value,状态主权就交还给了应用层,这是全选主复选框可靠派生状态的前提。
4.3 在 Form.Item 中落地全选
Checkbox 的值属性是 checked 而非 value,因此若要在表单中收集勾选状态,需要借助 valuePropName="checked"(官方文档 FAQ 已给出模板 index.zh-CN.md):
import { Checkbox, Form } from 'antd';
const App: React.FC = () => (
<Form.Item name="agree" valuePropName="checked">
<Checkbox>我已阅读并同意协议</Checkbox>
</Form.Item>
);
该约束同样适用于「表单内的全选」:主复选框的受控数据应直接来自表单状态,从而与 Form 的校验、提交逻辑打通。
五、总结
- 全选 = 受控数组 + 派生布尔值。官方 check-all.tsx 用
checkedList.length分别推导checkAll与indeterminate,主复选框自身不保存状态。 indeterminate是「只负责样式控制」的属性,Checkbox.tsx 通过useEffect把它同步到原生input.indeterminateDOM property,并附加ant-checkbox-indeterminate样式类,交互层面用户无法直接进入该状态。- 半选状态必须由数据驱动、可被用户勾选行为消除:点击主复选框后立即将子项集合整体置空或整体选满,即可让半选消失,完成一次标准的「全选/全不选」闭环。
如果想继续深入,推荐在仓库中继续阅读同一目录下的 group.tsx 演示(了解 Group 基本受控用法)以及 GroupContext.ts(理解 Group 如何通过 Context 向子 Checkbox 分发注册与勾选逻辑),从而把全选模式推广到更复杂的动态选项列表。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00