react-checkbox md 使用文档

33 阅读10分钟

Checkbox 复选框技术文档

用于在一组可选项中进行多选的 React 组件,参考 Ant Design 5 实现标准。

目录


组件概览

组件结构

checkbox/
├── index.tsx              # 入口文件,导出 Checkbox 和 Checkbox.Group
├── Checkbox.tsx           # 单个复选框组件
├── CheckboxGroup.tsx      # 复选框组组件
├── context.tsx            # React Context 定义
├── index.scss             # 样式文件
├── index.test.tsx         # 测试文件
├── Checkbox.stories.tsx   # Storybook 故事
└── README.md             # 本文档

技术栈

  • React 18: 函数组件 + Hooks
  • TypeScript: 完整的类型支持和泛型
  • classnames: 动态 className 管理
  • SCSS: 样式系统,支持 CSS 变量
  • Jest + Testing Library: 测试框架
  • Storybook: 组件展示和开发

核心特性

受控/非受控模式:支持完全受控和非受控两种模式
三态支持:checked、unchecked、indeterminate
泛型支持:value 类型可定制(string、number、object 等)
Context 通信:CheckboxGroup 与 Checkbox 通过 Context 通信
Ref 转发:支持 forwardRef 访问原生 input 元素
性能优化:React.memo + useCallback + useRef
无障碍访问:完整的 ARIA 属性支持
主题定制:基于 CSS 变量的主题系统
表单集成:支持 name、id、autoFocus 等表单属性
测试覆盖:完整的单元测试


架构设计

整体架构

┌─────────────────────────────────────┐
│        index.tsx (入口)              │
│  - 导出 Checkbox                     │
│  - 导出 Checkbox.Group               │
│  - 导出所有类型                      │
└─────────────────────────────────────┘
              │
              ├──────────────────┬──────────────────┐
              │                  │                  │
              ▼                  ▼                  ▼
  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────┐
  │  Checkbox.tsx    │  │CheckboxGroup.tsx │  │ context.tsx  │
  │                  │  │                  │  │              │
  │ - 单个复选框     │  │ - 管理多个复选框 │  │ - Context    │
  │ - 状态管理       │  │ - 统一状态       │  │ - 事件接口   │
  │ - 事件处理       │  │ - Options 模式   │  │ - 类型定义   │
  │ - Ref 转发       │  │ - Context 提供   │  │              │
  └──────────────────┘  └──────────────────┘  └──────────────┘

组件通信

单独使用 Checkbox:

┌────────────────────┐
│   Parent State     │
│  [checked, setXxx] │
└─────────┬──────────┘
          │ props
          ▼
    ┌──────────┐
    │ Checkbox │
    └──────────┘

CheckboxGroup + Checkbox:

┌────────────────────────────────┐
│      CheckboxGroup             │
│  - value: string[]             │
│  - onChange: (arr) => void     │
└─────┬──────────────────────────┘
      │
      │ Context.Provider
      │  { value, onChange, disabled }
      ▼
┌─────────────────────────────────┐
│  ┌──────────┐  ┌──────────┐    │
│  │Checkbox 1│  │Checkbox 2│    │
│  │useContext│  │useContext│    │
│  └──────────┘  └──────────┘    │
└─────────────────────────────────┘

状态管理模式

1. 非受控模式(Uncontrolled)
// 组件内部管理状态
<Checkbox defaultChecked>选项</Checkbox>

内部实现:

const [checked, setCheck] = useState(props.defaultChecked || false);
2. 受控模式(Controlled)
// 外部完全控制状态
const [checked, setChecked] = useState(false);
<Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />;

内部实现:

useEffect(() => {
  if ("checked" in props && props.checked !== undefined) {
    setCheck(props.checked);
  }
}, [props.checked]);
3. CheckboxGroup 受控
const [values, setValues] = useState(["1", "2"]);
<CheckboxGroup value={values} onChange={setValues}>
  <Checkbox value="1">选项1</Checkbox>
  <Checkbox value="2">选项2</Checkbox>
</CheckboxGroup>;

核心实现

1. Checkbox 组件核心逻辑

状态管理
// 内部状态
const [checked, setCheck] = useState(props.defaultChecked || false);
const checkedRef = useRef(checked);

// 同步 ref(用于事件处理中获取最新状态)
useEffect(() => {
  checkedRef.current = checked;
}, [checked]);

// 受控模式同步
useEffect(() => {
  if ("checked" in props && props.checked !== undefined) {
    setCheck(props.checked);
  }
}, [props.checked]);

// CheckboxGroup 控制
useEffect(() => {
  if (values && "value" in props) {
    setCheck(values.indexOf(props.value) > -1);
  }
}, [values, props.value]);

关键技术点:

  • 使用 "checked" in props 判断是否为受控模式
  • 使用 checkedRef 存储最新状态,解决闭包问题
  • 三个 useEffect 分别处理:自身状态、受控模式、Group 控制
事件处理
const handleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
  if (disabled || cdisabled) {
    return;
  }

  const state = !checkedRef.current;

  // 非受控模式才更新内部状态
  if (!("checked" in props)) {
    setCheck(state);
  }

  // 创建标准化事件对象
  const checkboxChangeEvent: CheckboxChangeEvent<T> = {
    target: {
      checked: state,
      value: value as T,
    },
    nativeEvent: e,
  };

  // 触发自身 onChange
  if (typeof onChange === "function") {
    onChange(checkboxChangeEvent);
  }

  // 触发 Group 的 onChange
  if (typeof conChange === "function") {
    conChange(checkboxChangeEvent);
  }
};

关键技术点:

  • 点击在最外层 <span> 上处理,而非 <input>
  • 创建自定义事件对象,而不是直接修改原生事件
  • 同时触发组件自身和 Group 的回调
  • 受控模式下不更新内部状态,由父组件控制
Indeterminate 状态
// 同步 indeterminate 到原生 input
useEffect(() => {
  if (inputEl.current) {
    inputEl.current.indeterminate = indeterminate;
  }
}, [indeterminate]);

// 样式处理
const cls = classNames({
  [`${prefixCls}checkbox`]: true,
  [`${prefixCls}checkbox-checked`]: checked && !indeterminate,
  [`${prefixCls}checkbox-indeterminate`]: indeterminate,
});

关键技术点:

  • indeterminate 只能通过 DOM API 设置,不能通过 HTML 属性
  • indeterminate 优先级高于 checked
  • ARIA 属性为 aria-checked="mixed"
Ref 转发
ref={(node) => {
  // 合并内部 ref 和外部 ref
  (inputEl as React.MutableRefObject<HTMLInputElement | null>).current = node;

  if (typeof ref === "function") {
    ref(node);
  } else if (ref) {
    (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
  }
}}

关键技术点:

  • 同时维护内部 ref(用于 indeterminate)和外部 ref
  • 支持函数 ref 和对象 ref 两种形式
  • 类型断言处理 TypeScript 类型检查
泛型支持
export interface CheckboxProps<T = string> {
  value?: T;
  onChange?: (e: CheckboxChangeEvent<T>) => void;
}

function InternalCheckbox<T = string>(
  props: CheckboxProps<T>,
  ref: React.Ref<HTMLInputElement>
) { ... }

// 使用
<Checkbox<number> value={1} />
<Checkbox<{id: string}> value={{id: '1'}} />

关键技术点:

  • 泛型默认值为 string
  • 支持任意类型的 value
  • 配合 TypeScript 提供完整的类型推导

2. CheckboxGroup 组件核心逻辑

状态管理
const [value, setValue] = useState<T[]>(
  props.defaultValue || props.value || []
);

// 使用 ref 保存最新值,优化 useCallback
const valueRef = useRef<T[]>(value);
const onChangeRef = useRef(onChange);

useEffect(() => {
  valueRef.current = value;
}, [value]);

useEffect(() => {
  onChangeRef.current = onChange;
}, [onChange]);

// 受控模式同步
useEffect(() => {
  if ("value" in props && props.value !== undefined) {
    setValue(props.value);
  }
}, [props.value]);

关键技术点:

  • 使用 ref 保存最新状态,避免 useCallback 频繁重建
  • 支持受控/非受控模式
事件处理(性能优化版)
const handleChange = useCallback((e: CheckboxChangeEvent<T>) => {
  const targetValue = e.target.value;
  const checked = e.target.checked;
  let newValue = [...valueRef.current]; // 使用 ref 读取最新值

  if (checked) {
    if (!newValue.includes(targetValue)) {
      newValue.push(targetValue);
    }
  } else {
    newValue = newValue.filter((item) => item !== targetValue);
  }

  setValue(newValue);
  onChangeRef.current?.(newValue); // 使用 ref 调用最新回调
}, []); // 空依赖数组,函数永远不会重新创建

关键技术点:

  • 使用 ref 替代直接依赖 state 和 props
  • 空依赖数组,避免 handleChange 重新创建
  • 减少子组件不必要的 re-render
Options 模式
const renderOptions = () => {
  if (!options) return null;

  return options.map((option, index) => {
    const isObject = typeof option === "object";
    const optionValue = isObject ? option.value : option;
    const optionLabel = isObject ? option.label : String(option);
    const optionDisabled = isObject ? option.disabled : false;

    return (
      <span key={`checkbox-option-${index}`}>
        <Checkbox
          checked={value.includes(optionValue)}
          disabled={disabled || optionDisabled}
          value={optionValue}
          onChange={handleChange}
          name={name}
        >
          {optionLabel}
        </Checkbox>
      </span>
    );
  });
};

关键技术点:

  • 支持简单数组:['A', 'B', 'C']
  • 支持对象数组:[{label: 'A', value: 'a', disabled: true}]
  • 自动类型判断,灵活处理
Context 提供
<CheckboxContext.Provider
  value={{
    onChange: handleChange,
    disabled: disabled || false,
    value,
  }}
>
  {options ? renderOptions() : children}
</CheckboxContext.Provider>

3. Context 设计

export interface CheckboxChangeEventTarget<T = string> {
  value: T;
  checked: boolean;
}

export interface CheckboxChangeEvent<T = string> {
  target: CheckboxChangeEventTarget<T>;
  nativeEvent?: React.MouseEvent<HTMLSpanElement>;
}

export interface CheckboxContextProps<T = string> {
  value: Array<T>;
  onChange: (e: CheckboxChangeEvent<T>) => void;
  disabled: boolean;
}

关键技术点:

  • 自定义事件对象,符合 React 表单惯例
  • 泛型支持,类型安全
  • 包含原生事件引用,方便高级用法

性能优化

1. React.memo

const CheckboxWithRef = forwardRef(InternalCheckbox);
const Checkbox = memo(CheckboxWithRef);

优化效果:

  • 避免 props 未变化时的不必要 re-render
  • 配合 CheckboxGroup 使用时效果明显

注意顺序:

  • 必须先 forwardRef,再 memo
  • 否则会导致 ref 无法正确转发

2. useCallback + useRef 优化

优化前(每次 re-render 都会重建函数):

const handleChange = useCallback(
  (e) => {
    // ... 使用 value 和 onChange
  },
  [value, onChange]
); // 依赖频繁变化

优化后(函数永远不会重建):

const valueRef = useRef(value);
const onChangeRef = useRef(onChange);

useEffect(() => {
  valueRef.current = value;
}, [value]);
useEffect(() => {
  onChangeRef.current = onChange;
}, [onChange]);

const handleChange = useCallback((e) => {
  // ... 使用 valueRef.current 和 onChangeRef.current
}, []); // 空依赖

优化收益:

  • handleChange 稳定,不会导致子组件 re-render
  • CheckboxGroup 内的所有 Checkbox 受益

3. checkedRef 解决闭包问题

const [checked, setCheck] = useState(false);
const checkedRef = useRef(checked);

useEffect(() => {
  checkedRef.current = checked;
}, [checked]);

const handleClick = (e) => {
  const state = !checkedRef.current; // 始终读取最新值
  // ...
};

解决的问题:

  • handleClick 中的 checked 可能是旧值
  • 使用 ref 确保读取最新状态

4. 样式优化

  • 使用 classnames 库动态生成 className,避免字符串拼接
  • CSS 变量支持主题切换,无需 JS 重新渲染
  • SCSS 编译为 CSS,运行时零成本

可访问性

ARIA 属性

aria-checked
aria-checked={indeterminate ? "mixed" : checked ? "true" : "false"}
  • "true": 选中
  • "false": 未选中
  • "mixed": 半选(indeterminate)
aria-disabled
aria-disabled={disabled || cdisabled}

键盘支持

原生 <input type="checkbox"> 天然支持:

  • Space: 切换选中状态
  • Tab: 焦点导航
  • Shift+Tab: 反向导航

表单集成

<input
  type="checkbox"
  name={name} // 表单字段名
  id={id} // 配合 label 使用
  autoFocus={autoFocus} // 自动聚焦
  tabIndex={tabIndex} // 自定义 Tab 顺序
/>

语义化 HTML

<span class="checkbox-wrapper">
  <span class="checkbox">
    <input type="checkbox" />
    <!-- 原生控件 -->
    <span class="checkbox-inner"></span>
    <!-- 自定义样式 -->
  </span>
  <span>文本标签</span>
</span>

开发指南

开发环境要求

  • Node.js >= 14
  • React >= 16.8
  • TypeScript >= 4.0

本地开发

# 安装依赖
npm install

# 启动 Storybook
npm run storybook

# 运行测试
npm test

# 测试覆盖率
npm test -- --coverage

目录结构说明

src/checkbox/
├── index.tsx              # 入口,导出组合组件
├── Checkbox.tsx           # 核心组件
├── CheckboxGroup.tsx      # Group 组件
├── context.tsx            # Context 和类型定义
├── index.scss             # 样式
├── index.test.tsx         # 单元测试
├── Checkbox.stories.tsx   # Storybook 故事
├── CheckboxGroup.stories.tsx
└── README.md             # 本文档

代码风格

  • 命名规范

    • 组件使用 PascalCase: Checkbox, CheckboxGroup
    • Props 接口使用 XxxProps: CheckboxProps, GroupProps
    • 事件类型使用 XxxEvent: CheckboxChangeEvent
  • 注释规范

    • Props 使用 JSDoc 注释
    • 复杂逻辑添加行内注释
    • 公开 API 必须有完整注释
  • 类型安全

    • 所有 Props 定义 interface
    • 导出所有公开类型
    • 使用泛型增强灵活性

添加新功能

  1. 修改类型定义:在 CheckboxProps 中添加新属性
  2. 实现逻辑:在 Checkbox.tsx 中实现
  3. 添加测试:在 index.test.tsx 中添加测试用例
  4. 更新文档:在本 README 中更新 API 文档
  5. 添加 Story:在 .stories.tsx 中添加演示

样式开发

CSS 变量命名规范:

--checkbox-{property}-{state}: value;

// 示例
--checkbox-primary-color: #1890ff;
--checkbox-disabled-bg: #f5f5f5;

BEM 命名规范:

.ant-checkbox {
} // Block
.ant-checkbox-checked {
} // Block--Modifier
.ant-checkbox-inner {
} // Block__Element
.ant-checkbox-wrapper {
} // Block-wrapper
.ant-checkbox-wrapper-disabled {
} // Block-wrapper--Modifier

测试覆盖

测试用例列表

基础功能测试
  • ✅ 基本渲染
  • ✅ 点击切换状态
  • ✅ 受控模式
  • ✅ defaultChecked
  • ✅ disabled 状态
  • ✅ indeterminate 状态
表单集成测试
  • ✅ name 属性
  • ✅ id 属性
  • ✅ autoFocus
  • ✅ tabIndex
  • ✅ Ref 转发
可访问性测试
  • ✅ aria-checked 属性
  • ✅ aria-disabled 属性
  • ✅ 动态更新 indeterminate
CheckboxGroup 测试
  • ✅ defaultValue
  • ✅ 受控 value
  • ✅ options 简单数组
  • ✅ options 对象数组
  • ✅ Group disabled
  • ✅ onChange 回调
样式测试
  • ✅ className 自定义
  • ✅ style 自定义

运行测试

# 运行所有测试
npm test

# 运行单个文件
npm test -- src/checkbox/index.test.tsx

# 查看覆盖率
npm test -- --coverage

# 监听模式
npm test -- --watch

测试覆盖率目标

  • Statements: > 90%
  • Branches: > 85%
  • Functions: > 90%
  • Lines: > 90%

使用文档

何时使用

  • 在一组可选项中进行多项选择时
  • 单独使用可以表示两种状态之间的切换,和 switch 类似。区别在于切换 switch 会直接触发状态改变,而 checkbox 一般用于状态标记,需要配合提交操作

代码演示

基本用法

import { Checkbox } from "./checkbox";

function App() {
  const onChange = (e) => {
    console.log(`checked = ${e.target.checked}`);
  };

  return <Checkbox onChange={onChange}>Checkbox</Checkbox>;
}

受控模式

function App() {
  const [checked, setChecked] = useState(false);

  return (
    <Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)}>
      {checked ? "已选中" : "未选中"}
    </Checkbox>
  );
}

不可用

<>
  <Checkbox defaultChecked={false} disabled>
    未选中禁用
  </Checkbox>
  <Checkbox defaultChecked disabled>
    选中禁用
  </Checkbox>
</>

Checkbox 组

方便的从数组生成 Checkbox 组。

function App() {
  const onChange = (checkedValues) => {
    console.log("checked = ", checkedValues);
  };

  return (
    <CheckboxGroup onChange={onChange}>
      <Checkbox value="1">选项1</Checkbox>
      <Checkbox value="2">选项2</Checkbox>
      <Checkbox value="3">选项3</Checkbox>
    </CheckboxGroup>
  );
}

使用 options

通过 options 属性简化使用。

const options = ["Apple", "Pear", "Orange"];

function App() {
  return <CheckboxGroup options={options} />;
}
const options = [
  { label: "Apple", value: "apple" },
  { label: "Pear", value: "pear" },
  { label: "Orange", value: "orange" },
];

function App() {
  return <CheckboxGroup options={options} />;
}

全选

实现全选效果。

function App() {
  const [checkedList, setCheckedList] = useState(["Apple"]);
  const options = ["Apple", "Pear", "Orange"];

  const checkAll = checkedList.length === options.length;
  const indeterminate =
    checkedList.length > 0 && checkedList.length < options.length;

  const onCheckAllChange = (e) => {
    setCheckedList(e.target.checked ? options : []);
  };

  return (
    <>
      <Checkbox
        indeterminate={indeterminate}
        checked={checkAll}
        onChange={onCheckAllChange}
      >
        全选
      </Checkbox>
      <CheckboxGroup
        options={options}
        value={checkedList}
        onChange={setCheckedList}
      />
    </>
  );
}

表单集成

<form>
  <label htmlFor="terms">
    <Checkbox id="terms" name="terms" autoFocus>
      我同意用户协议
    </Checkbox>
  </label>
  <Checkbox id="newsletter" name="newsletter">
    订阅新闻邮件
  </Checkbox>
</form>

使用 Ref

function App() {
  const checkboxRef = useRef<HTMLInputElement>(null);

  const handleFocus = () => {
    checkboxRef.current?.focus();
  };

  return (
    <>
      <Checkbox ref={checkboxRef}>使用 Ref 的 Checkbox</Checkbox>
      <Button onClick={handleFocus}>聚焦到 Checkbox</Button>
    </>
  );
}

泛型支持

Checkbox 支持泛型,可以使用任意类型的 value。

// 数字类型
<Checkbox<number> value={1}>选项1</Checkbox>

// 对象类型
interface Option {
  id: string;
  name: string;
}
<Checkbox<Option> value={{ id: '1', name: 'Option 1' }}>
  选项1
</Checkbox>

// CheckboxGroup 也支持泛型
<CheckboxGroup<number>
  options={[1, 2, 3]}
  onChange={(values) => {
    // values 的类型是 number[]
  }}
/>

API

Checkbox

参数说明类型默认值
autoFocus自动获取焦点booleanfalse
checked指定当前是否选中booleanfalse
defaultChecked初始是否选中booleanfalse
disabled失效状态booleanfalse
indeterminate设置 indeterminate 状态,只负责样式控制booleanfalse
onChange变化时的回调函数(e: CheckboxChangeEvent) => void-
value根据 value 进行比较,判断是否选中T-
nameinput[type="checkbox"] 元素的 name 属性string-
idinput[type="checkbox"] 元素的 id 属性string-
tabIndexTab 键控制次序number-
className自定义类名string-
style自定义样式React.CSSProperties-

Checkbox.Group

参数说明类型默认值
defaultValue默认选中的选项T[][]
disabled整组失效booleanfalse
nameCheckboxGroup 下所有 input[type="checkbox"] 的 name 属性string-
options指定可选项T[] | CheckboxOptionType[][]
value指定选中的选项T[][]
onChange变化时的回调函数(checkedValue: T[]) => void-
direction排列方向'horizontal' | 'vertical''horizontal'
className自定义类名string-
style自定义样式React.CSSProperties-

CheckboxOptionType

参数说明类型默认值
label选项的文本string-
value选项的值T-
disabled是否禁用booleanfalse
style自定义样式React.CSSProperties-
className自定义类名string-

CheckboxChangeEvent

interface CheckboxChangeEvent<T = string> {
  target: {
    checked: boolean;
    value: T;
  };
  nativeEvent?: React.MouseEvent<HTMLSpanElement>;
}

方法

Checkbox

名称说明
blur()移除焦点
focus()获取焦点

通过 ref 访问:

const ref = useRef<HTMLInputElement>(null);

<Checkbox ref={ref}>选项</Checkbox>;

ref.current?.focus(); // 聚焦
ref.current?.blur(); // 失焦

主题定制

Checkbox 支持通过 CSS 变量进行主题定制:

:root {
  --checkbox-primary-color: #1890ff; /* 主题色 */
  --checkbox-border-color: #d9d9d9; /* 边框颜色 */
  --checkbox-bg-color: #fff; /* 背景颜色 */
  --checkbox-text-color: #000000d9; /* 文本颜色 */
  --checkbox-disabled-bg: #f5f5f5; /* 禁用背景色 */
  --checkbox-disabled-text: #00000040; /* 禁用文本色 */
  --checkbox-disabled-border: #d9d9d9; /* 禁用边框色 */
}

自定义主题示例

/* 红色主题 */
.red-theme {
  --checkbox-primary-color: #ff4d4f;
}

/* 绿色主题 */
.green-theme {
  --checkbox-primary-color: #52c41a;
}

/* 暗色主题 */
.dark-theme {
  --checkbox-primary-color: #177ddc;
  --checkbox-bg-color: #141414;
  --checkbox-border-color: #434343;
  --checkbox-text-color: rgba(255, 255, 255, 0.85);
}

使用:

<div className="red-theme">
  <Checkbox>红色主题</Checkbox>
</div>

注意事项

  1. 受控模式 vs 非受控模式

    • 使用 checked 属性时,组件变为受控模式,需要配合 onChange 更新状态
    • 使用 defaultChecked 时,组件为非受控模式,由组件内部管理状态
  2. indeterminate 状态

    • indeterminate 只负责样式显示,不影响 checked 状态
    • 通常用于全选功能,表示部分选中的状态
  3. CheckboxGroup 使用

    • 在 CheckboxGroup 中使用 Checkbox 时,应该指定 value 属性
    • CheckboxGroup 的 value 是一个数组,包含所有选中的 Checkbox 的 value
  4. 性能优化

    • Checkbox 使用了 React.memo 进行性能优化
    • CheckboxGroup 使用 ref 优化了 useCallback 依赖
  5. 类型安全

    • 使用 TypeScript 时,可以通过泛型指定 value 的类型
    • 默认类型为 string

FAQ

为什么我的 Checkbox 点击没有反应?

如果使用了 checked 属性(受控模式),需要配合 onChange 来更新状态:

// ❌ 错误:受控模式但没有更新状态
<Checkbox checked={checked}>选项</Checkbox>

// ✅ 正确
<Checkbox
  checked={checked}
  onChange={(e) => setChecked(e.target.checked)}
>
  选项
</Checkbox>

如何实现全选功能?

使用 indeterminate 属性:

const [checkedList, setCheckedList] = useState([]);
const options = ["A", "B", "C"];

const checkAll = checkedList.length === options.length;
const indeterminate =
  checkedList.length > 0 && checkedList.length < options.length;

<Checkbox
  indeterminate={indeterminate}
  checked={checkAll}
  onChange={(e) => setCheckedList(e.target.checked ? options : [])}
>
  全选
</Checkbox>;

如何禁用某些选项?

在 CheckboxGroup 中使用 options 时:

const options = [
  { label: "选项1", value: "1" },
  { label: "选项2", value: "2", disabled: true }, // 禁用此选项
  { label: "选项3", value: "3" },
];

<CheckboxGroup options={options} />;

如何自定义主题颜色?

通过 CSS 变量覆盖:

.my-checkbox {
  --checkbox-primary-color: #ff6b6b;
}
<div className="my-checkbox">
  <Checkbox>自定义颜色</Checkbox>
</div>

技术细节深入

受控与非受控的判断逻辑

// 判断是否为受控模式
const isControlled = "checked" in props;

// 受控模式:不更新内部状态
if (!isControlled) {
  setCheck(state);
}

// 同步受控 props
useEffect(() => {
  if (isControlled && props.checked !== undefined) {
    setCheck(props.checked);
  }
}, [props.checked]);

为什么使用 "checked" in props 而不是 props.checked !== undefined

  • undefined 也是合法的受控值
  • "checked" in props 检查属性是否存在于 props 对象中
  • 更符合 React 官方的受控判断方式

Context 的作用域

CheckboxGroup (Context.Provider)
  ├─ Checkbox (消费 Context)
  ├─ Checkbox (消费 Context)
  └─ SomeDiv
       └─ Checkbox (仍然消费 Context)

关键点:

  • Context 会穿透中间的非 Checkbox 组件
  • 任何嵌套的 Checkbox 都会受 CheckboxGroup 控制
  • 如果不想被 Group 控制,需要在外层使用独立的 Checkbox

事件对象的设计哲学

为什么不直接修改原生事件?

// ❌ 不好的做法
const handleClick = (e: React.MouseEvent) => {
  e.target.checked = state; // 修改原生事件对象
  onChange(e);
};

// ✅ 正确做法
const handleClick = (e: React.MouseEvent) => {
  const checkboxEvent: CheckboxChangeEvent<T> = {
    target: {
      checked: state,
      value: value as T,
    },
    nativeEvent: e,
  };
  onChange(checkboxEvent);
};

原因:

  1. 类型安全:自定义事件对象有明确的类型定义
  2. 解耦:不依赖原生 DOM 结构
  3. 可扩展:可以添加额外信息而不污染原生事件
  4. 符合惯例:与 Ant Design 等主流库一致

泛型类型的边界情况

// 场景 1: 使用对象作为 value
interface UserOption {
  id: number;
  name: string;
}

<CheckboxGroup<UserOption>
  value={[{ id: 1, name: "Alice" }]}
  onChange={(values) => {
    // values 类型是 UserOption[]
  }}
>
  <Checkbox<UserOption> value={{ id: 1, name: "Alice" }}>Alice</Checkbox>
</CheckboxGroup>;

// 注意:对象比较使用的是引用相等
// 需要确保同一个对象引用

对象类型 value 的最佳实践:

// 推荐:使用唯一 ID 作为 value
<Checkbox value={user.id}>{user.name}</Checkbox>

// 不推荐:使用整个对象
<Checkbox value={user}>{user.name}</Checkbox>

indeterminate 的实现原理

HTML <input type="checkbox"> 有一个特殊的 indeterminate 属性:

  • 不能通过 HTML 属性设置
  • 只能通过 JavaScript DOM API 设置
  • 不影响 checked
  • 仅用于视觉表现
// 这样不起作用
<input type="checkbox" indeterminate={true} />;

// 必须通过 DOM API
useEffect(() => {
  if (inputEl.current) {
    inputEl.current.indeterminate = indeterminate;
  }
}, [indeterminate]);

Ref 转发的复杂性

为什么需要合并 ref?

// 内部 ref:用于设置 indeterminate
const inputEl = useRef<HTMLInputElement>(null);

// 外部 ref:用户通过 forwardRef 传入
// 需要同时满足两者

ref={(node) => {
  // 1. 设置内部 ref
  inputEl.current = node;

  // 2. 设置外部 ref(支持函数和对象两种形式)
  if (typeof ref === "function") {
    ref(node);
  } else if (ref) {
    ref.current = node;
  }
}}

为什么 forwardRef 必须在 memo 之前?

// ❌ 错误顺序
const MemoizedCheckbox = memo(InternalCheckbox);
const Checkbox = forwardRef(MemoizedCheckbox); // ref 会丢失

// ✅ 正确顺序
const CheckboxWithRef = forwardRef(InternalCheckbox);
const Checkbox = memo(CheckboxWithRef); // ref 正确转发

最佳实践

1. 受控模式的完整使用

function MyForm() {
  const [agreed, setAgreed] = useState(false);

  const handleSubmit = () => {
    if (!agreed) {
      alert("请先同意条款");
      return;
    }
    // 提交表单
  };

  return (
    <form>
      <Checkbox checked={agreed} onChange={(e) => setAgreed(e.target.checked)}>
        我同意服务条款
      </Checkbox>
      <button onClick={handleSubmit}>提交</button>
    </form>
  );
}

2. CheckboxGroup 的推荐使用方式

function FilterPanel() {
  const [selectedTags, setSelectedTags] = useState<string[]>([]);

  const tagOptions = [
    { label: "React", value: "react" },
    { label: "Vue", value: "vue" },
    { label: "Angular", value: "angular", disabled: true },
  ];

  const handleFilterChange = (values: string[]) => {
    setSelectedTags(values);
    // 触发数据过滤
    fetchData({ tags: values });
  };

  return (
    <CheckboxGroup
      options={tagOptions}
      value={selectedTags}
      onChange={handleFilterChange}
      direction="vertical"
    />
  );
}

3. 全选/反选的完整实现

function SelectAllExample() {
  const allOptions = ["Apple", "Banana", "Orange", "Grape"];
  const [checkedList, setCheckedList] = useState<string[]>(["Apple"]);

  const checkAll = checkedList.length === allOptions.length;
  const indeterminate =
    checkedList.length > 0 && checkedList.length < allOptions.length;

  const handleCheckAllChange = (e: CheckboxChangeEvent<string>) => {
    setCheckedList(e.target.checked ? allOptions : []);
  };

  const handleGroupChange = (values: string[]) => {
    setCheckedList(values);
  };

  return (
    <>
      <Checkbox
        indeterminate={indeterminate}
        checked={checkAll}
        onChange={handleCheckAllChange}
      >
        全选 ({checkedList.length}/{allOptions.length})
      </Checkbox>
      <Divider />
      <CheckboxGroup
        options={allOptions}
        value={checkedList}
        onChange={handleGroupChange}
        direction="vertical"
      />
    </>
  );
}

4. 表单集成的最佳实践

function RegistrationForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const termsRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    // 验证复选框
    if (!termsRef.current?.checked) {
      termsRef.current?.focus();
      alert("请同意服务条款");
      return;
    }

    // 获取表单数据
    const formData = new FormData(formRef.current!);
    console.log("terms:", formData.get("terms"));
    console.log("newsletter:", formData.get("newsletter"));
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <Checkbox ref={termsRef} name="terms" id="terms" defaultChecked={false}>
        我同意 <a href="/terms">服务条款</a>
      </Checkbox>

      <Checkbox name="newsletter" defaultChecked>
        订阅新闻邮件
      </Checkbox>

      <button type="submit">注册</button>
    </form>
  );
}

5. 性能优化的实际案例

// 场景:大量复选框(100+)

function LargeCheckboxList() {
  const [selectedIds, setSelectedIds] = useState<number[]>([]);

  // ✅ 使用 options 模式,避免手动渲染
  const options = useMemo(
    () =>
      Array.from({ length: 100 }, (_, i) => ({
        label: `选项 ${i + 1}`,
        value: i,
      })),
    []
  );

  // ✅ CheckboxGroup 内部已使用 useCallback + ref 优化
  // onChange 不会导致子组件不必要的 re-render

  return (
    <CheckboxGroup
      options={options}
      value={selectedIds}
      onChange={setSelectedIds}
      direction="vertical"
    />
  );
}

// ❌ 不推荐:手动渲染大量 Checkbox
function BadExample() {
  const [selected, setSelected] = useState<number[]>([]);

  const handleChange = (value: number) => (e: CheckboxChangeEvent<number>) => {
    // 这个函数每次渲染都会重新创建
    setSelected(
      e.target.checked
        ? [...selected, value]
        : selected.filter((v) => v !== value)
    );
  };

  return (
    <div>
      {Array.from({ length: 100 }).map((_, i) => (
        <Checkbox key={i} value={i} onChange={handleChange(i)}>
          选项 {i}
        </Checkbox>
      ))}
    </div>
  );
}

6. 主题定制的最佳实践

// theme.scss - 定义主题变量
.theme-red {
  --checkbox-primary-color: #ff4d4f;
  --checkbox-border-color: #ff7875;
}

.theme-dark {
  --checkbox-primary-color: #177ddc;
  --checkbox-bg-color: #141414;
  --checkbox-border-color: #434343;
  --checkbox-text-color: rgba(255, 255, 255, 0.85);
  --checkbox-disabled-bg: #1f1f1f;
  --checkbox-disabled-text: rgba(255, 255, 255, 0.3);
  --checkbox-disabled-border: #434343;
}
// 使用主题
function ThemedCheckbox() {
  const [theme, setTheme] = useState("default");

  return (
    <div className={`theme-${theme}`}>
      <Checkbox>主题化的复选框</Checkbox>

      <select onChange={(e) => setTheme(e.target.value)}>
        <option value="default">默认</option>
        <option value="red">红色</option>
        <option value="dark">暗色</option>
      </select>
    </div>
  );
}

常见陷阱与解决方案

1. 受控模式下忘记更新状态

问题:

// ❌ 点击没有反应
const [checked, setChecked] = useState(false);
<Checkbox checked={checked}>选项</Checkbox>;

解决:

// ✅ 提供 onChange
const [checked, setChecked] = useState(false);
<Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)}>
  选项
</Checkbox>;

2. CheckboxGroup 中忘记设置 value

问题:

// ❌ 无法被 Group 管理
<CheckboxGroup>
  <Checkbox>选项 1</Checkbox> {/* 缺少 value */}
  <Checkbox>选项 2</Checkbox>
</CheckboxGroup>

解决:

// ✅ 必须设置 value
<CheckboxGroup>
  <Checkbox value="1">选项 1</Checkbox>
  <Checkbox value="2">选项 2</Checkbox>
</CheckboxGroup>

3. 对象 value 的比较问题

问题:

// ❌ 对象引用不同,永远不会选中
const user1 = { id: 1, name: "Alice" };
const user2 = { id: 1, name: "Alice" };

<CheckboxGroup value={[user1]}>
  <Checkbox value={user2}>Alice</Checkbox> {/* user1 !== user2 */}
</CheckboxGroup>;

解决:

// ✅ 方案 1:使用同一个对象引用
const user = { id: 1, name: 'Alice' };
<CheckboxGroup value={[user]}>
  <Checkbox value={user}>Alice</Checkbox>
</CheckboxGroup>

// ✅ 方案 2:使用简单类型(推荐)
<CheckboxGroup value={[1]}>
  <Checkbox value={1}>Alice</Checkbox>
</CheckboxGroup>

4. indeterminate 与 checked 的混淆

问题:

// ❌ indeterminate 不会改变 checked 的值
<Checkbox indeterminate checked={false}>
  {/* 视觉上是半选,但 checked 仍然是 false */}
</Checkbox>

理解:

  • indeterminate 只是视觉状态,不影响实际的 checked
  • 适用场景:全选功能中的"部分选中"状态
  • 需要同时管理 checkedindeterminate 两个状态

5. ref 的类型错误

问题:

// ❌ 类型不匹配
const ref = useRef<HTMLSpanElement>(null);
<Checkbox ref={ref}>选项</Checkbox>;

解决:

// ✅ ref 指向的是 input 元素
const ref = useRef<HTMLInputElement>(null);
<Checkbox ref={ref}>选项</Checkbox>;

// 使用
ref.current?.focus();
ref.current?.checked; // boolean

6. 在 useEffect 中使用事件对象

问题:

// ❌ React 事件对象会被复用
const handleChange = (e: CheckboxChangeEvent<string>) => {
  setTimeout(() => {
    console.log(e.target.checked); // 可能不是预期的值
  }, 1000);
};

解决:

// ✅ 立即提取需要的值
const handleChange = (e: CheckboxChangeEvent<string>) => {
  const checked = e.target.checked;
  const value = e.target.value;

  setTimeout(() => {
    console.log(checked, value); // 正确
  }, 1000);
};

版本更新记录

v2.0.0 (当前版本)

重大更新:

  • ✅ 完全重写,对齐 Ant Design 5 标准
  • ✅ 添加 TypeScript 泛型支持
  • ✅ 实现 React.memo 性能优化
  • ✅ 添加完整的 ARIA 无障碍支持
  • ✅ 支持 CSS 变量主题定制
  • ✅ forwardRef 支持访问原生 input

新增功能:

  • indeterminate 属性
  • name, id, autoFocus, tabIndex 表单属性
  • CheckboxGroup.options 对象数组支持
  • className, style 自定义样式支持

性能优化:

  • CheckboxGroup 使用 useRef + useCallback 优化
  • Checkbox 使用 React.memo 避免不必要渲染
  • checkedRef 解决闭包问题

破坏性变更:

  • onChange 事件对象结构变化
  • Context 类型定义变化(添加泛型)

v1.0.0 (初始版本)

  • 基础 Checkbox 组件
  • 基础 CheckboxGroup 组件
  • 基本的受控/非受控支持

贡献指南

提交 Bug

提交 Issue 时请包含:

  1. 复现步骤
  2. 预期行为
  3. 实际行为
  4. 环境信息(React 版本、浏览器等)
  5. 最小可复现代码

提交功能请求

  1. 描述功能需求
  2. 说明使用场景
  3. 提供设计方案(可选)
  4. 参考其他组件库的实现(可选)

Pull Request 规范

  1. Fork 仓库并创建分支
  2. 编写代码,遵循现有代码风格
  3. 添加/更新测试用例
  4. 更新文档
  5. 确保所有测试通过
  6. 提交 PR,描述修改内容
# 开发流程
git checkout -b feature/my-feature
npm test
npm run storybook
# 开发和测试
git commit -m "feat: add new feature"
git push origin feature/my-feature

参考资料

相关标准

参考实现

相关文章


License

MIT


最后更新: 2025-10-27
维护者: paopao 组件库团队
版本: 2.0.0