The useMultipleSelection hook provides functionality for managing multiple item selection state. It's designed to compose with other downshift hooks to create complex multi-selection scenarios like tag inputs, multi-select dropdowns, and transfer lists.
Creates a multiple selection component that manages selected items and active index state.
/**
* Hook for managing multiple item selection state
* @param props - Configuration options for multiple selection
* @returns Object containing state, actions, and prop getters
*/
function useMultipleSelection<Item>(
props?: UseMultipleSelectionProps<Item>
): UseMultipleSelectionReturnValue<Item>;
interface UseMultipleSelectionProps<Item> {
/** Array of currently selected items */
selectedItems?: Item[];
/** Initial selected items when component mounts */
initialSelectedItems?: Item[];
/** Default selected items for uncontrolled usage */
defaultSelectedItems?: Item[];
/** Function to generate a unique key for each item */
itemToKey?: (item: Item | null) => any;
/** Function to generate accessibility status messages */
getA11yStatusMessage?: (options: UseMultipleSelectionState<Item>) => string;
/** Custom state reducer for advanced state management */
stateReducer?: (
state: UseMultipleSelectionState<Item>,
actionAndChanges: UseMultipleSelectionStateChangeOptions<Item>
) => Partial<UseMultipleSelectionState<Item>>;
/** Index of currently active selected item */
activeIndex?: number;
/** Initial active index when component mounts */
initialActiveIndex?: number;
/** Default active index for uncontrolled usage */
defaultActiveIndex?: number;
/** Callback when active index changes */
onActiveIndexChange?: (changes: UseMultipleSelectionActiveIndexChange<Item>) => void;
/** Callback when selected items change */
onSelectedItemsChange?: (changes: UseMultipleSelectionSelectedItemsChange<Item>) => void;
/** Callback when any state changes */
onStateChange?: (changes: UseMultipleSelectionStateChange<Item>) => void;
/** Key for navigating to next item (default: 'ArrowRight') */
keyNavigationNext?: string;
/** Key for navigating to previous item (default: 'ArrowLeft') */
keyNavigationPrevious?: string;
/** Environment object for SSR/testing scenarios */
environment?: Environment;
}
interface UseMultipleSelectionReturnValue<Item> {
/** Array of currently selected items */
selectedItems: Item[];
/** Index of currently active selected item (-1 if none) */
activeIndex: number;
/** Actions for controlling the selection programmatically */
reset: () => void;
addSelectedItem: (item: Item) => void;
removeSelectedItem: (item: Item) => void;
setSelectedItems: (items: Item[]) => void;
setActiveIndex: (index: number) => void;
/** Prop getters for UI elements */
getSelectedItemProps: <Options>(
options: UseMultipleSelectionGetSelectedItemPropsOptions<Item> & Options
) => UseMultipleSelectionGetSelectedItemReturnValue;
getDropdownProps: <Options>(
options?: UseMultipleSelectionGetDropdownPropsOptions & Options,
extraOptions?: GetPropsCommonOptions
) => UseMultipleSelectionGetDropdownReturnValue;
}Usage Examples:
import { useMultipleSelection } from 'downshift';
// Basic tag input example
function TagInput() {
const [availableItems] = useState(['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']);
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
activeIndex,
} = useMultipleSelection({
initialSelectedItems: [],
});
const [inputValue, setInputValue] = useState('');
const handleKeyDown = (e) => {
if (e.key === 'Enter' && inputValue) {
const item = availableItems.find(
item => item.toLowerCase().includes(inputValue.toLowerCase()) &&
!selectedItems.includes(item)
);
if (item) {
addSelectedItem(item);
setInputValue('');
}
}
};
return (
<div>
<label>Selected items:</label>
<div {...getDropdownProps()} style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{selectedItems.map((selectedItem, index) => (
<span
key={`selected-item-${index}`}
{...getSelectedItemProps({ selectedItem, index })}
style={{
backgroundColor: activeIndex === index ? '#bde4ff' : '#e1e5e9',
padding: '4px 8px',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{selectedItem}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeSelectedItem(selectedItem);
}}
style={{ marginLeft: '4px', border: 'none', background: 'transparent' }}
>
×
</button>
</span>
))}
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type to add items..."
/>
</div>
<div>Available: {availableItems.filter(item => !selectedItems.includes(item)).join(', ')}</div>
</div>
);
}
// Advanced example with custom state reducer
function AdvancedMultipleSelection() {
const items = [
{ id: 1, name: 'Apple', category: 'fruit' },
{ id: 2, name: 'Banana', category: 'fruit' },
{ id: 3, name: 'Carrot', category: 'vegetable' },
{ id: 4, name: 'Broccoli', category: 'vegetable' },
];
const stateReducer = (state, actionAndChanges) => {
const { changes, type } = actionAndChanges;
switch (type) {
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
// Prevent deletion of certain items
if (changes.selectedItems && changes.selectedItems.length !== state.selectedItems.length) {
const removedItem = state.selectedItems.find(item => !changes.selectedItems.includes(item));
if (removedItem && removedItem.category === 'fruit') {
return state; // Prevent removal of fruits
}
}
return changes;
default:
return changes;
}
};
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
activeIndex,
} = useMultipleSelection({
initialSelectedItems: [],
stateReducer,
onSelectedItemsChange: ({ selectedItems }) => {
console.log('Selected items changed:', selectedItems);
},
});
return (
<div>
<label>Multiple Selection Example:</label>
<div {...getDropdownProps()} style={{ border: '1px solid #ccc', padding: '8px', minHeight: '40px' }}>
{selectedItems.map((selectedItem, index) => (
<span
key={selectedItem.id}
{...getSelectedItemProps({ selectedItem, index })}
style={{
backgroundColor: activeIndex === index ? '#bde4ff' : '#e1e5e9',
padding: '4px 8px',
margin: '2px',
borderRadius: '4px',
display: 'inline-block',
}}
>
{selectedItem.name} ({selectedItem.category})
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeSelectedItem(selectedItem);
}}
style={{ marginLeft: '4px', border: 'none', background: 'transparent' }}
>
×
</button>
</span>
))}
</div>
<div style={{ marginTop: '8px' }}>
<strong>Available items:</strong>
{items
.filter(item => !selectedItems.some(selected => selected.id === item.id))
.map(item => (
<button
key={item.id}
onClick={() => addSelectedItem(item)}
style={{ margin: '2px', padding: '4px 8px' }}
>
Add {item.name}
</button>
))}
</div>
</div>
);
}The hook returns current state and action functions for programmatic control.
interface UseMultipleSelectionState<Item> {
/** Array of currently selected items */
selectedItems: Item[];
/** Index of currently active selected item (-1 if none) */
activeIndex: number;
}
interface UseMultipleSelectionActions<Item> {
/** Reset to initial state */
reset: () => void;
/** Add an item to the selection */
addSelectedItem: (item: Item) => void;
/** Remove an item from the selection */
removeSelectedItem: (item: Item) => void;
/** Replace all selected items */
setSelectedItems: (items: Item[]) => void;
/** Set the active index */
setActiveIndex: (index: number) => void;
}Prop getters return props to spread onto DOM elements with proper event handlers and accessibility attributes.
interface UseMultipleSelectionGetSelectedItemPropsOptions<Item> extends React.HTMLProps<HTMLElement> {
/** The selected item this props object is for */
selectedItem: Item;
/** Index of the selected item in the selectedItems array */
index?: number;
/** Custom ref key (defaults to 'ref') */
refKey?: string;
}
interface UseMultipleSelectionGetSelectedItemReturnValue {
/** Tab index for keyboard navigation */
tabIndex: 0 | -1;
ref?: React.RefObject<any>;
onClick: React.MouseEventHandler;
onKeyDown: React.KeyboardEventHandler;
}
interface UseMultipleSelectionGetDropdownPropsOptions extends React.HTMLProps<HTMLElement> {
/** Whether to prevent default keyboard actions */
preventKeyAction?: boolean;
}
interface UseMultipleSelectionGetDropdownReturnValue {
ref?: React.RefObject<any>;
onClick?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
}Constants for identifying different types of state changes in the state reducer.
enum UseMultipleSelectionStateChangeTypes {
SelectedItemClick = '__selected_item_click__',
SelectedItemKeyDownDelete = '__selected_item_keydown_delete__',
SelectedItemKeyDownBackspace = '__selected_item_keydown_backspace__',
SelectedItemKeyDownNavigationNext = '__selected_item_keydown_navigation_next__',
SelectedItemKeyDownNavigationPrevious = '__selected_item_keydown_navigation_previous__',
DropdownKeyDownNavigationPrevious = '__dropdown_keydown_navigation_previous__',
DropdownKeyDownBackspace = '__dropdown_keydown_backspace__',
DropdownClick = '__dropdown_click__',
FunctionAddSelectedItem = '__function_add_selected_item__',
FunctionRemoveSelectedItem = '__function_remove_selected_item__',
FunctionSetSelectedItems = '__function_set_selected_items__',
FunctionSetActiveIndex = '__function_set_active_index__',
FunctionReset = '__function_reset__',
}Access via useMultipleSelection.stateChangeTypes:
import { useMultipleSelection } from 'downshift';
const stateReducer = (state, actionAndChanges) => {
switch (actionAndChanges.type) {
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
// Handle backspace key on selected items
return actionAndChanges.changes;
case useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem:
// Handle programmatic addition of items
return {
...actionAndChanges.changes,
activeIndex: actionAndChanges.changes.selectedItems.length - 1,
};
default:
return actionAndChanges.changes;
}
};The useMultipleSelection hook is designed to compose with other downshift hooks:
import { useCombobox, useMultipleSelection } from 'downshift';
// Example: Multi-select combobox
function MultiSelectCombobox() {
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
const [inputItems, setInputItems] = useState(items);
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
} = useMultipleSelection({
initialSelectedItems: [],
});
const getFilteredItems = (items, selectedItems, inputValue) => {
return items.filter(
item =>
!selectedItems.includes(item) &&
item.toLowerCase().startsWith(inputValue.toLowerCase())
);
};
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectItem,
} = useCombobox({
items: inputItems,
itemToString: (item) => item || '',
stateReducer: (state, actionAndChanges) => {
const { changes, type } = actionAndChanges;
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // Keep menu open after selection
highlightedIndex: 0,
inputValue: '',
};
default:
return changes;
}
},
onStateChange: ({ inputValue, type, selectedItem }) => {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
if (selectedItem) {
addSelectedItem(selectedItem);
setInputItems(getFilteredItems(items, [...selectedItems, selectedItem], ''));
}
break;
case useCombobox.stateChangeTypes.InputChange:
setInputItems(getFilteredItems(items, selectedItems, inputValue));
break;
default:
break;
}
},
});
return (
<div>
<label {...getLabelProps()}>Choose multiple items:</label>
<div {...getDropdownProps()}>
{selectedItems.map((selectedItem, index) => (
<span
key={`selected-item-${index}`}
{...getSelectedItemProps({ selectedItem, index })}
style={{
backgroundColor: '#e1e5e9',
padding: '2px 6px',
margin: '2px',
borderRadius: '3px',
}}
>
{selectedItem}
<span
onClick={() => removeSelectedItem(selectedItem)}
style={{ marginLeft: '4px', cursor: 'pointer' }}
>
×
</span>
</span>
))}
<input {...getInputProps()} />
</div>
<button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
{isOpen ? '↑' : '↓'}
</button>
<ul {...getMenuProps()}>
{isOpen &&
inputItems.map((item, index) => (
<li
style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
))}
</ul>
</div>
);
}The hook provides built-in accessibility features:
Custom accessibility messages can be provided:
const { selectedItems } = useMultipleSelection({
getA11yStatusMessage: ({ selectedItems }) => {
return `${selectedItems.length} items selected. Use arrow keys to navigate, backspace to remove items.`;
},
});