Benutzerdefinierter React-Hook zur Verwaltung des Auswahlzustands mehrerer Elemente. Behandelt die drei Zustände vollständig ausgewählt, teilweise ausgewählt und nichts ausgewählt typsicher.
Mehrfachauswahl wird schnell unübersichtlich. Eine Tabelle mit Sammelaktionen braucht meist ausgewählte IDs, eine Header-Checkbox, einen Zwischenzustand und einen sauberen Weg, den Auswahlmodus zu verlassen. useSelection bündelt diese Logik an einer Stelle und passt damit gut zu Admin-Tabellen, Dateilisten und ähnlichen Oberflächen.
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;}Wofür der Hook passt
Er eignet sich für Listen, in denen Einzelwahl und ein globales Alles-auswählen gemeinsam vorkommen. Weil der Hook selectedCount, status und isIndeterminate zusammen zurückgibt, kann die View-Schicht schlank bleiben.
Warum die Struktur hilfreich ist
SelectionStatus hält die drei Zustände none, some und all zusammen, statt diese Logik in die UI zu verteilen. keyAccessor vermeidet Probleme mit Objektidentität, wenn Items bei jedem Render neu erzeugt werden. toggleMode() leert die Auswahl beim Verlassen des Auswahlmodus, was Sammelaktionen einfacher zurücksetzt.
Hinweise
selectAll() kopiert das aktuelle items-Array und arbeitet damit bewusst nur auf der aktuellen Seite. Wenn die Oberfläche seitenübergreifende Auswahl oder ein servergestütztes Auswahlmodell braucht, sollte lokaler Auswahlzustand von einem separaten globalen Auswahlzustand getrennt werden.
hsb.horse