首页
/ ant-design Checkbox 全选(check-all)实战:indeterminate 半选状态原理与实现详解

ant-design Checkbox 全选(check-all)实战:indeterminate 半选状态原理与实现详解

2026-09-06 18:38:12作者:曹令琨Iris

在实现「全选 / 反选」这类交互时,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 需要同时绑定 checkedindeterminate 两个属性的原因。

二、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;

这份代码揭示了全选功能需要维护的两条数据流

  1. 子项 → 主复选框(单向派生):子项组 CheckboxGroup 的任何勾选变化,都会通过其 onChange(list) 回调更新 checkedList。主复选框不持有独立状态,它的 checkedindeterminate 完全由 checkedList 与总选项数实时推导,保证主复选框永远忠实反映子项选择结果。
  2. 主复选框 → 子项(一次性批量设置):点击主复选框时,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.mdindeterminate 的完整定义为:

参数 说明 类型 默认值
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

当选项包含 disabledlabelvalue 分离时,需要基于 value 进行全选判定。可参考 Group.tsxCheckboxOptionType 的定义(labelvaluedisabledstyleclassNametitle 等字段):

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.tsxcheckedList.length 分别推导 checkAllindeterminate,主复选框自身不保存状态。
  • indeterminate 是「只负责样式控制」的属性,Checkbox.tsx 通过 useEffect 把它同步到原生 input.indeterminate DOM property,并附加 ant-checkbox-indeterminate 样式类,交互层面用户无法直接进入该状态。
  • 半选状态必须由数据驱动、可被用户勾选行为消除:点击主复选框后立即将子项集合整体置空或整体选满,即可让半选消失,完成一次标准的「全选/全不选」闭环。

如果想继续深入,推荐在仓库中继续阅读同一目录下的 group.tsx 演示(了解 Group 基本受控用法)以及 GroupContext.ts(理解 Group 如何通过 Context 向子 Checkbox 分发注册与勾选逻辑),从而把全选模式推广到更复杂的动态选项列表。

登录后查看全文
热门项目推荐
相关项目推荐