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
- 组件使用 PascalCase:
-
注释规范:
- Props 使用 JSDoc 注释
- 复杂逻辑添加行内注释
- 公开 API 必须有完整注释
-
类型安全:
- 所有 Props 定义 interface
- 导出所有公开类型
- 使用泛型增强灵活性
添加新功能
- 修改类型定义:在
CheckboxProps中添加新属性 - 实现逻辑:在
Checkbox.tsx中实现 - 添加测试:在
index.test.tsx中添加测试用例 - 更新文档:在本 README 中更新 API 文档
- 添加 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 | 自动获取焦点 | boolean | false |
| checked | 指定当前是否选中 | boolean | false |
| defaultChecked | 初始是否选中 | boolean | false |
| disabled | 失效状态 | boolean | false |
| indeterminate | 设置 indeterminate 状态,只负责样式控制 | boolean | false |
| onChange | 变化时的回调函数 | (e: CheckboxChangeEvent) => void | - |
| value | 根据 value 进行比较,判断是否选中 | T | - |
| name | input[type="checkbox"] 元素的 name 属性 | string | - |
| id | input[type="checkbox"] 元素的 id 属性 | string | - |
| tabIndex | Tab 键控制次序 | number | - |
| className | 自定义类名 | string | - |
| style | 自定义样式 | React.CSSProperties | - |
Checkbox.Group
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| defaultValue | 默认选中的选项 | T[] | [] |
| disabled | 整组失效 | boolean | false |
| name | CheckboxGroup 下所有 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 | 是否禁用 | boolean | false |
| 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>
注意事项
-
受控模式 vs 非受控模式
- 使用
checked属性时,组件变为受控模式,需要配合onChange更新状态 - 使用
defaultChecked时,组件为非受控模式,由组件内部管理状态
- 使用
-
indeterminate 状态
indeterminate只负责样式显示,不影响checked状态- 通常用于全选功能,表示部分选中的状态
-
CheckboxGroup 使用
- 在 CheckboxGroup 中使用 Checkbox 时,应该指定
value属性 - CheckboxGroup 的
value是一个数组,包含所有选中的 Checkbox 的value
- 在 CheckboxGroup 中使用 Checkbox 时,应该指定
-
性能优化
- Checkbox 使用了
React.memo进行性能优化 - CheckboxGroup 使用 ref 优化了
useCallback依赖
- Checkbox 使用了
-
类型安全
- 使用 TypeScript 时,可以通过泛型指定
value的类型 - 默认类型为
string
- 使用 TypeScript 时,可以通过泛型指定
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);
};
原因:
- 类型安全:自定义事件对象有明确的类型定义
- 解耦:不依赖原生 DOM 结构
- 可扩展:可以添加额外信息而不污染原生事件
- 符合惯例:与 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值- 适用场景:全选功能中的"部分选中"状态
- 需要同时管理
checked和indeterminate两个状态
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 时请包含:
- 复现步骤
- 预期行为
- 实际行为
- 环境信息(React 版本、浏览器等)
- 最小可复现代码
提交功能请求
- 描述功能需求
- 说明使用场景
- 提供设计方案(可选)
- 参考其他组件库的实现(可选)
Pull Request 规范
- Fork 仓库并创建分支
- 编写代码,遵循现有代码风格
- 添加/更新测试用例
- 更新文档
- 确保所有测试通过
- 提交 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