Reactで複数アイテムの選択状態を管理するカスタムフック。全選択、部分選択、個別選択の3状態を型安全に扱える。
import { useCallback, useState } from "react";
export type SelectionStatus = "none" | "some" | "all";
export interface UseSelection<T> { // State readonly isSelectMode: boolean; readonly selectedItems: T[];
readonly selectedCount: number; readonly status: SelectionStatus;
readonly isAllSelected: boolean; readonly isIndeterminate: boolean;
// Actions toggleMode: () => void; toggleAll: () => void; selectAll: () => void; clear: () => void; isSelected: (item: T) => boolean; onSelect: (item: T) => void;}
type SelectionState<T> = { isSelectMode: boolean; selectedItems: T[];};
export const SelectionStatus = { all: "all", none: "none", some: "some",} as const satisfies Record<SelectionStatus, SelectionStatus>;
export const useSelection = <T>( items: T[], keyAccessor?: (item: T) => string | number,): UseSelection<T> => { const [state, setState] = useState<SelectionState<T>>({ isSelectMode: false, selectedItems: [], });
const totalCount = items.length; const selectedCount = state.selectedItems.length;
const status: SelectionStatus = toSelectionStatus(selectedCount, totalCount);
const isAllSelected = status === SelectionStatus.all; const isIndeterminate = status === SelectionStatus.some;
const getKey = useCallback( (item: T) => (keyAccessor ? keyAccessor(item) : item), [keyAccessor], );
const toggleMode = useCallback(() => { setState((prev) => ({ isSelectMode: !prev.isSelectMode, selectedItems: prev.isSelectMode ? [] : prev.selectedItems, })); }, []);
const selectAll = useCallback(() => { setState((prev) => ({ ...prev, selectedItems: [...items] })); }, [items]);
const clear = useCallback(() => { setState((prev) => ({ ...prev, selectedItems: [] })); }, []);
const toggleAll = useCallback(() => { setState((prev) => { const currentCount = prev.selectedItems.length; const total = items.length;
let currentStatus: SelectionStatus = SelectionStatus.none; if (total > 0 && currentCount === total) currentStatus = SelectionStatus.all; else if (currentCount > 0) currentStatus = SelectionStatus.some;
const shouldSelectAll = currentStatus !== SelectionStatus.all;
return { ...prev, selectedItems: shouldSelectAll ? [...items] : [], }; }); }, [items]);
const onSelect = useCallback( (item: T) => { const key = getKey(item); setState((prev) => { const isSelected = prev.selectedItems.some((i) => getKey(i) === key); const nextSelectedItems = isSelected ? prev.selectedItems.filter((i) => getKey(i) !== key) : [...prev.selectedItems, item];
return { ...prev, selectedItems: nextSelectedItems }; }); }, [getKey], );
const isSelected = useCallback( (item: T) => { const key = getKey(item); return state.selectedItems.some((i) => getKey(i) === key); }, [state.selectedItems, getKey], );
return { isSelectMode: state.isSelectMode, selectedItems: state.selectedItems, selectedCount, status, isAllSelected, isIndeterminate, toggleMode, toggleAll, selectAll, clear, isSelected, onSelect, };};
function toSelectionStatus(selected: number, total: number): SelectionStatus { if (total === 0 || selected === 0) return SelectionStatus.none; if (selected === total) return SelectionStatus.all; return SelectionStatus.some;}
hsb.horse