Hook React personnalisé pour gérer l’état de sélection de plusieurs éléments. Prend en charge de manière type-safe les trois états : tout sélectionné, partiellement sélectionné, rien sélectionné.
Les interfaces de sélection multiple se compliquent vite. Une table avec actions groupées demande souvent un tableau d’identifiants sélectionnés, une case à cocher d’en-tête, un état indéterminé et une façon propre de sortir du mode sélection. useSelection regroupe cette logique au même endroit, ce qui fonctionne bien pour des tables d’administration, des listes de fichiers ou des listes de cartes.
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;}Où ce hook convient
Il est utile dans les listes qui combinent sélection individuelle et contrôle global de type tout sélectionner. Comme le hook renvoie selectedCount, status et isIndeterminate ensemble, la couche de vue peut rester légère.
Pourquoi cette structure est pratique
SelectionStatus aligne les trois états none, some et all, sans obliger le composant à refaire cette logique. keyAccessor évite de dépendre de l’identité des objets quand les éléments sont recréés à chaque rendu. toggleMode() vide aussi la sélection quand on sort du mode sélection, ce qui simplifie le flux des actions groupées.
Notes
selectAll() copie le tableau items courant. Le modèle de sélection reste donc volontairement local à la page. Si l’interface doit gérer une sélection sur plusieurs pages ou synchronisée avec le serveur, mieux vaut séparer l’état de sélection local d’un état de sélection global distinct.
hsb.horse