Created
March 16, 2026 08:26
-
-
Save librz/516fa85d50f014ad381899c34827a39c to your computer and use it in GitHub Desktop.
A react hook for handling list select (including support for select all)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { groupBy } from "es-toolkit"; | |
| import { useCallback, useMemo, useState } from "react"; | |
| type SelectAllControl = { | |
| disabled: boolean; | |
| checkedState: boolean | "indeterminate"; | |
| onToggleSelect: () => void; | |
| }; | |
| type ItemSelectControl<T> = { | |
| isSelected: (item: T) => boolean; | |
| onToggleSelect: (item: T) => void; | |
| }; | |
| export type ListSelectControl<T> = { | |
| selectedItems: T[]; | |
| all: SelectAllControl; | |
| item: ItemSelectControl<T>; | |
| clearSelect: () => void; | |
| }; | |
| /** | |
| * hooks for implementing select in list | |
| * @param items list data | |
| * - provide current page list if select all mode is current-page | |
| * - provide list of all pages if select all mode is cross-page | |
| * - pass in filtered items if there's search involved | |
| * @param idKey name of id that uniquely identifies a list item | |
| * @param options.isItemDisabled whether an item is disabled or not | |
| * @returns select control | |
| */ | |
| export function useListSelect<T, K extends keyof T>( | |
| items: T[], | |
| idKey: K, | |
| options?: { | |
| isItemDisabled?: (item: T) => boolean; | |
| }, | |
| ): ListSelectControl<T> { | |
| const { isItemDisabled } = options ?? {}; | |
| const selectableItems: T[] = useMemo(() => { | |
| if (!isItemDisabled) { | |
| return items; | |
| } | |
| return items.filter((it) => !isItemDisabled(it)); | |
| }, [items, isItemDisabled]); | |
| /** | |
| * using map instead of array for perf (O(1) when search, delete or append) | |
| */ | |
| const [selectedItemsMap, setSelectedItemsMap] = useState<Map<T[K], T>>(new Map()); | |
| const selectedItems: T[] = useMemo(() => { | |
| return Array.from(selectedItemsMap.values()); | |
| }, [selectedItemsMap]); | |
| const selectAllDisabled: boolean = selectableItems.length === 0; | |
| const selectAllChecked: boolean | "indeterminate" = useMemo(() => { | |
| if (selectableItems.length === 0) return false; | |
| const { selected = [], unselected = [] } = groupBy(selectableItems, (it) => | |
| selectedItemsMap.has(it[idKey]) ? "selected" : "unselected", | |
| ); | |
| if (selected.length === 0) { | |
| return false; | |
| } | |
| if (unselected.length === 0) { | |
| return true; | |
| } | |
| return "indeterminate"; | |
| }, [selectableItems, selectedItemsMap, idKey]); | |
| const isItemSelected = useCallback( | |
| (item: T) => { | |
| const id = item[idKey]; | |
| return selectedItemsMap.has(id); | |
| }, | |
| [selectedItemsMap, idKey], | |
| ); | |
| const clearSelect = useCallback(() => { | |
| setSelectedItemsMap(new Map()); | |
| }, []); | |
| const toggleItemSelect = useCallback( | |
| (item: T) => { | |
| setSelectedItemsMap((map) => { | |
| const newMap = new Map(map); | |
| const id = item[idKey]; | |
| if (map.has(id)) { | |
| newMap.delete(id); | |
| } else { | |
| newMap.set(id, item); | |
| } | |
| return newMap; | |
| }); | |
| }, | |
| [idKey], | |
| ); | |
| const toggleSelectAll = useCallback(() => { | |
| setSelectedItemsMap((map) => { | |
| const newMap = new Map(map); | |
| const allItemsSelected = selectableItems.every((it) => map.has(it[idKey])); | |
| selectableItems.forEach((it) => { | |
| const mapKey = it[idKey]; | |
| if (allItemsSelected) { | |
| // action: uncheck all | |
| newMap.delete(mapKey); | |
| } else { | |
| // action: check all | |
| newMap.set(mapKey, it); | |
| } | |
| }); | |
| return newMap; | |
| }); | |
| }, [selectableItems, idKey]); | |
| const selectAllControl: SelectAllControl = { | |
| disabled: selectAllDisabled, | |
| checkedState: selectAllChecked, | |
| onToggleSelect: toggleSelectAll, | |
| }; | |
| const itemSelectControl: ItemSelectControl<T> = { | |
| isSelected: isItemSelected, | |
| onToggleSelect: toggleItemSelect, | |
| }; | |
| const selectControl: ListSelectControl<T> = { | |
| selectedItems, | |
| clearSelect, | |
| all: selectAllControl, | |
| item: itemSelectControl, | |
| }; | |
| return selectControl; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment