引言
在现代Web应用中,多选组件是常见的UI元素,尤其是在需要用户从多个选项中进行选择的场景。本文将介绍如何使用React和TypeScript实现一个功能完整、性能优化的多列多选组件,支持"Select All"功能和垂直填充的多列布局。

组件功能介绍
本文实现的MultiCheck组件具有以下功能:
- 基础多选功能:支持用户选择多个选项
- "Select All"功能:一键全选/取消全选所有选项
- 多列布局:支持垂直填充的多列排列
- 受控组件设计:支持外部控制选中状态
- 性能优化:使用React Hooks优化渲染性能
- 类型安全:完整的TypeScript类型定义
技术栈
- React 18:使用函数组件和Hooks
- TypeScript:提供类型安全
- CSS Grid:实现灵活的多列布局
- React Hooks :
useState,useEffect,useCallback
核心实现
1. 类型定义
首先,我们定义组件的类型,确保类型安全:
typescript
export type Option = {
label: string,
value: string
}
type Props = {
label?: string,
options: Option[],
columns?: number,
values?: string[]
onChange?: (options: Option[]) => void,
}
2. 状态管理
使用useState钩子管理选中状态,并通过useEffect实现与外部状态的同步:
typescript
const [selectedValues, setSelectedValues] = useState<string[]>(values);
// 监听外部values变化,实现双向更新
useEffect(() => {
setSelectedValues(values);
}, [values]);
3. 全选功能实现
实现"Select All"功能需要以下逻辑:
typescript
// 判断是否全选
const isAllSelected = options.length > 0 && selectedValues.length === options.length;
// 处理全选事件
const handleSelectAllChange = useCallback(() => {
const newSelected = isAllSelected ? [] : options.map(opt => opt.value);
setSelectedValues(newSelected);
if (onChange) {
const selectedOptions = isAllSelected ? [] : [...options];
onChange(selectedOptions);
}
}, [options, isAllSelected, onChange]);
4. 多列垂直填充布局
使用CSS Grid实现垂直填充的多列布局:
typescript
<div className='div-option-container' style={{
display: 'grid',
gridTemplateColumns: `repeat(${Math.min(columns, options.length + 1)}, auto)`,
gridTemplateRows: `repeat(${Math.ceil((options.length+1) / columns)}, auto)`,
gridAutoFlow: "column", // 关键:实现垂直填充
width: '100%'
}}>
{/* 选项内容 */}
</div>
5. 性能优化
使用useCallback缓存事件处理函数,避免不必要的重新渲染:
typescript
const handleOptionChange = useCallback((value: string) => {
setSelectedValues(prev => {
const newSelected = prev.includes(value)
? prev.filter(v => v !== value)
: [...prev, value];
if (onChange) {
const selectedOptions = options.filter(opt => newSelected.includes(opt.value));
onChange(selectedOptions);
}
return newSelected;
});
}, [options, onChange]);
完整组件代码
typescript
import './MultiCheck.css';
import React, { useState, useEffect, useCallback } from 'react';
export type Option = {
label: string,
value: string
}
type Props = {
label?: string,
options: Option[],
columns?: number,
values?: string[]
onChange?: (options: Option[]) => void,
}
const MultiCheck: React.FunctionComponent<Props> = ({
label,
options,
columns = 1,
values = [],
onChange
}): JSX.Element => {
const [selectedValues, setSelectedValues] = useState<string[]>(values);
useEffect(() => {
setSelectedValues(values);
}, [values]);
const isAllSelected = options.length > 0 && selectedValues.length === options.length;
const handleSelectAllChange = useCallback(() => {
const newSelected = isAllSelected ? [] : options.map(opt => opt.value);
setSelectedValues(newSelected);
if (onChange) {
const selectedOptions = isAllSelected ? [] : [...options];
onChange(selectedOptions);
}
}, [options, isAllSelected, onChange]);
const handleOptionChange = useCallback((value: string) => {
setSelectedValues(prev => {
const newSelected = prev.includes(value)
? prev.filter(v => v !== value)
: [...prev, value];
if (onChange) {
const selectedOptions = options.filter(opt => newSelected.includes(opt.value));
onChange(selectedOptions);
}
return newSelected;
});
}, [options, onChange]);
return(
<div className='div-container' style={{ width: `${200*columns}px` }}>
{label && <div className='div-label-container'>{label}</div>}
<div className='div-option-container' style={{
display: 'grid',
gridTemplateColumns: `repeat(${Math.min(columns, options.length + 1)}, auto)`,
gridTemplateRows: `repeat(${Math.ceil((options.length+1) / columns)}, auto)`,
gridAutoFlow: "column",
width: '100%'
}}>
<div className='div-option'>
<input
type="checkbox"
id="select-all"
checked={isAllSelected}
onChange={handleSelectAllChange}
/>
<label htmlFor="select-all">Select All</label>
</div>
{options.map(option => (
<div key={option.value} className='div-option'>
<input
type="checkbox"
id={`option-${option.value}`}
checked={selectedValues.includes(option.value)}
onChange={() => handleOptionChange(option.value)}
className='div-option-checkbox'
/>
<label htmlFor={`option-${option.value}`}>{option.label}</label>
</div>
))}
</div>
</div>
)
}
export default MultiCheck;
使用示例
typescript
import React from 'react';
import MultiCheck, { Option } from './MultiCheck';
const App: React.FC = () => {
const options: Option[] = [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' },
{ label: '选项4', value: '4' },
{ label: '选项5', value: '5' },
{ label: '选项6', value: '6' },
];
const handleChange = (selectedOptions: Option[]) => {
console.log('选中的选项:', selectedOptions);
};
return (
<div className="App">
<MultiCheck
label="多选组件示例"
options={options}
columns={2}
values={['2', '4']}
onChange={handleChange}
/>
</div>
);
};
export default App;
性能优化关键点
- 使用
useCallback缓存事件处理函数:避免每次渲染都创建新函数 - 使用CSS Grid实现高效布局:减少JavaScript计算负担
- 合理设计依赖数组:确保Hooks只在必要时重新执行
- 函数式状态更新:避免闭包陷阱,确保基于最新状态更新
总结
本文实现的MultiCheck组件是一个功能完整、性能优化的多选组件,支持"Select All"功能和垂直填充的多列布局。通过使用React Hooks和TypeScript,我们实现了一个类型安全、性能优异的组件。
该组件的设计思路和实现方式可以作为开发其他复杂UI组件的参考,尤其是在需要处理大量选项和复杂布局的场景。
扩展建议
- 添加搜索功能:支持在大量选项中快速搜索
- 支持分组:实现选项的分组显示
- 添加键盘导航:提高可访问性
- 支持自定义样式:允许外部自定义组件样式
- 添加动画效果:提升用户体验
通过不断扩展和优化,这个组件可以适应更多复杂的业务场景,成为一个功能强大的多选组件库。