logo hsb.horse
← Retour à l’index des snippets

Snippets

Hook React useSelection

Hook React personnalisé pour gérer l’état de sélection de plusieurs éléments. Gère de façon type-safe la sélection totale, partielle et individuelle.

Publié: Mis à jour:

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.