The Downshift class component is the original render prop-based API for building accessible dropdown components. While the modern hook-based APIs are recommended for new projects, the class component remains fully supported and provides additional flexibility for complex use cases.
A React class component that uses the render prop pattern to provide complete control over dropdown behavior and rendering.
/**
* Class component for building accessible dropdown components using render props
*/
class Downshift<Item = any> extends React.Component<DownshiftProps<Item>> {
/** State change type constants for use in state reducers */
static stateChangeTypes: {
unknown: StateChangeTypes.unknown;
mouseUp: StateChangeTypes.mouseUp;
itemMouseEnter: StateChangeTypes.itemMouseEnter;
keyDownArrowUp: StateChangeTypes.keyDownArrowUp;
keyDownArrowDown: StateChangeTypes.keyDownArrowDown;
keyDownEscape: StateChangeTypes.keyDownEscape;
keyDownEnter: StateChangeTypes.keyDownEnter;
clickItem: StateChangeTypes.clickItem;
blurInput: StateChangeTypes.blurInput;
changeInput: StateChangeTypes.changeInput;
keyDownSpaceButton: StateChangeTypes.keyDownSpaceButton;
clickButton: StateChangeTypes.clickButton;
blurButton: StateChangeTypes.blurButton;
controlledPropUpdatedSelectedItem: StateChangeTypes.controlledPropUpdatedSelectedItem;
touchEnd: StateChangeTypes.touchEnd;
};
}
interface DownshiftProps<Item> {
/** Render prop function that receives state and helpers */
children?: (options: ControllerStateAndHelpers<Item>) => React.ReactNode;
/** Initial state values */
initialSelectedItem?: Item;
initialInputValue?: string;
initialHighlightedIndex?: number | null;
initialIsOpen?: boolean;
/** Default values for uncontrolled usage */
defaultHighlightedIndex?: number | null;
defaultIsOpen?: boolean;
/** Function to convert an item to its string representation */
itemToString?: (item: Item | null) => string;
/** Function to determine if selected item has changed */
selectedItemChanged?: (prevItem: Item, item: Item) => boolean;
/** Function to generate accessibility status messages */
getA11yStatusMessage?: (options: A11yStatusMessageOptions<Item>) => string;
/** Callback when selection changes */
onChange?: (
selectedItem: Item | null,
stateAndHelpers: ControllerStateAndHelpers<Item>
) => void;
/** Callback when item is selected (legacy, use onChange) */
onSelect?: (
selectedItem: Item | null,
stateAndHelpers: ControllerStateAndHelpers<Item>
) => void;
/** Callback when any state changes */
onStateChange?: (
options: StateChangeOptions<Item>,
stateAndHelpers: ControllerStateAndHelpers<Item>
) => void;
/** Callback when input value changes */
onInputValueChange?: (
inputValue: string,
stateAndHelpers: ControllerStateAndHelpers<Item>
) => void;
/** Custom state reducer for advanced state management */
stateReducer?: (
state: DownshiftState<Item>,
changes: StateChangeOptions<Item>
) => Partial<StateChangeOptions<Item>>;
/** Total number of items (for virtual scrolling scenarios) */
itemCount?: number;
/** Controlled state props */
highlightedIndex?: number | null;
inputValue?: string | null;
isOpen?: boolean;
selectedItem?: Item | null;
/** Custom IDs */
id?: string;
inputId?: string;
labelId?: string;
menuId?: string;
getItemId?: (index?: number) => string;
/** Environment for SSR/testing */
environment?: Environment;
/** Callback when clicking outside the component */
onOuterClick?: (stateAndHelpers: ControllerStateAndHelpers<Item>) => void;
/** Custom scroll function */
scrollIntoView?: (node: HTMLElement, menuNode: HTMLElement) => void;
/** Callback for user-initiated actions */
onUserAction?: (
options: StateChangeOptions<Item>,
stateAndHelpers: ControllerStateAndHelpers<Item>
) => void;
/** Suppress ref errors in development */
suppressRefError?: boolean;
}Usage Examples:
import Downshift from 'downshift';
// Basic dropdown example
function BasicDropdown() {
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
return (
<Downshift
onChange={selection => alert(`You selected ${selection}`)}
itemToString={item => (item ? item : '')}
>
{({
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
isOpen,
inputValue,
highlightedIndex,
selectedItem,
getRootProps,
}) => (
<div>
<label {...getLabelProps()}>Enter a fruit</label>
<div {...getRootProps()} style={{ position: 'relative' }}>
<input {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen
? items
.filter(item => !inputValue || item.includes(inputValue))
.map((item, index) => (
<li
{...getItemProps({
key: item,
index,
item,
style: {
backgroundColor: highlightedIndex === index ? 'lightgray' : 'white',
fontWeight: selectedItem === item ? 'bold' : 'normal',
},
})}
>
{item}
</li>
))
: null}
</ul>
</div>
</div>
)}
</Downshift>
);
}
// Advanced example with state reducer
function AdvancedDropdown() {
const items = [
{ id: 1, name: 'Apple', color: 'red' },
{ id: 2, name: 'Banana', color: 'yellow' },
{ id: 3, name: 'Cherry', color: 'red' },
];
const stateReducer = (state, changes) => {
switch (changes.type) {
case Downshift.stateChangeTypes.keyDownEnter:
case Downshift.stateChangeTypes.clickItem:
return {
...changes,
isOpen: state.isOpen, // Keep menu open after selection
highlightedIndex: state.highlightedIndex,
};
default:
return changes;
}
};
return (
<Downshift
onChange={selection => console.log('Selected:', selection)}
itemToString={item => (item ? item.name : '')}
stateReducer={stateReducer}
>
{({
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
getToggleButtonProps,
isOpen,
inputValue,
highlightedIndex,
selectedItem,
getRootProps,
}) => (
<div>
<label {...getLabelProps()}>Choose a fruit:</label>
<div {...getRootProps()} style={{ position: 'relative', display: 'inline-block' }}>
<input {...getInputProps()} placeholder="Type or click button" />
<button type="button" {...getToggleButtonProps()}>
{isOpen ? '↑' : '↓'}
</button>
<ul {...getMenuProps()} style={{ position: 'absolute', top: '100%', left: 0, right: 0 }}>
{isOpen
? items
.filter(item => !inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase()))
.map((item, index) => (
<li
{...getItemProps({
key: item.id,
index,
item,
})}
style={{
backgroundColor: highlightedIndex === index ? 'lightblue' : 'white',
fontWeight: selectedItem === item ? 'bold' : 'normal',
padding: '8px',
cursor: 'pointer',
}}
>
{item.name} ({item.color})
</li>
))
: null}
</ul>
</div>
{selectedItem && <div>Selected: {selectedItem.name}</div>}
</div>
)}
</Downshift>
);
}The render prop function receives a comprehensive object with state, actions, and prop getters:
interface ControllerStateAndHelpers<Item> extends DownshiftState<Item>, PropGetters<Item>, Actions<Item> {
/** Current component state */
highlightedIndex: number | null;
inputValue: string | null;
isOpen: boolean;
selectedItem: Item | null;
/** Action functions */
reset: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
openMenu: (cb?: Callback) => void;
closeMenu: (cb?: Callback) => void;
toggleMenu: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
selectItem: (item: Item | null, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
selectItemAtIndex: (index: number, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
selectHighlightedItem: (otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
setHighlightedIndex: (index: number, otherStateToSet?: Partial<StateChangeOptions<Item>>, cb?: Callback) => void;
clearSelection: (cb?: Callback) => void;
clearItems: () => void;
setItemCount: (count: number) => void;
unsetItemCount: () => void;
setState: (stateToSet: Partial<StateChangeOptions<Item>> | StateChangeFunction<Item>, cb?: Callback) => void;
itemToString: (item: Item | null) => string;
/** Prop getter functions */
getRootProps: <Options>(options?: GetRootPropsOptions & Options, otherOptions?: GetPropsCommonOptions) => GetRootPropsReturnValue;
getToggleButtonProps: <Options>(options?: GetToggleButtonPropsOptions & Options) => GetToggleButtonPropsReturnValue;
getLabelProps: <Options>(options?: GetLabelPropsOptions & Options) => GetLabelPropsReturnValue;
getMenuProps: <Options>(options?: GetMenuPropsOptions & Options, otherOptions?: GetPropsCommonOptions) => GetMenuPropsReturnValue;
getInputProps: <Options>(options?: GetInputPropsOptions & Options) => GetInputPropsReturnValue;
getItemProps: <Options>(options: GetItemPropsOptions<Item> & Options) => GetItemPropsReturnValue;
}The component maintains internal state that can be controlled or uncontrolled:
interface DownshiftState<Item> {
/** Index of currently highlighted item (null if none) */
highlightedIndex: number | null;
/** Current input value (null if no input) */
inputValue: string | null;
/** Whether dropdown menu is open */
isOpen: boolean;
/** Currently selected item (null if none) */
selectedItem: Item | null;
}The class component provides the same prop getters as the hooks, with additional options:
interface GetRootPropsOptions {
/** Custom ref key (defaults to 'ref') */
refKey?: string;
/** React ref object */
ref?: React.RefObject<any>;
}
interface GetRootPropsReturnValue {
'aria-expanded': boolean;
'aria-haspopup': 'listbox';
'aria-labelledby': string;
'aria-owns': string | undefined;
role: 'combobox';
ref?: React.RefObject<any>;
}
interface GetToggleButtonPropsOptions extends React.HTMLProps<HTMLButtonElement> {
/** Whether the button is disabled */
disabled?: boolean;
/** Event handler for React Native press events */
onPress?: (event: React.BaseSyntheticEvent) => void;
}
interface GetToggleButtonPropsReturnValue {
'aria-label': 'close menu' | 'open menu';
'aria-haspopup': true;
'data-toggle': true;
role: 'button';
type: 'button';
onPress?: (event: React.BaseSyntheticEvent) => void;
onClick?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onKeyUp?: React.KeyboardEventHandler;
onBlur?: React.FocusEventHandler;
}Constants for identifying different types of state changes in the state reducer:
enum StateChangeTypes {
unknown = '__autocomplete_unknown__',
mouseUp = '__autocomplete_mouseup__',
itemMouseEnter = '__autocomplete_item_mouseenter__',
keyDownArrowUp = '__autocomplete_keydown_arrow_up__',
keyDownArrowDown = '__autocomplete_keydown_arrow_down__',
keyDownEscape = '__autocomplete_keydown_escape__',
keyDownEnter = '__autocomplete_keydown_enter__',
keyDownHome = '__autocomplete_keydown_home__',
keyDownEnd = '__autocomplete_keydown_end__',
clickItem = '__autocomplete_click_item__',
blurInput = '__autocomplete_blur_input__',
changeInput = '__autocomplete_change_input__',
keyDownSpaceButton = '__autocomplete_keydown_space_button__',
clickButton = '__autocomplete_click_button__',
blurButton = '__autocomplete_blur_button__',
controlledPropUpdatedSelectedItem = '__autocomplete_controlled_prop_updated_selected_item__',
touchEnd = '__autocomplete_touchend__',
}Access via Downshift.stateChangeTypes:
import Downshift from 'downshift';
const stateReducer = (state, changes) => {
switch (changes.type) {
case Downshift.stateChangeTypes.keyDownEnter:
// Handle Enter key press
return changes;
case Downshift.stateChangeTypes.clickItem:
// Handle item click
return {
...changes,
isOpen: false, // Close menu after selection
};
default:
return changes;
}
};For new projects, consider using the modern hook-based APIs instead:
// Instead of this class component approach:
<Downshift itemToString={item => item.name}>
{({ getInputProps, getItemProps, isOpen, inputValue, highlightedIndex }) => (
// JSX here
)}
</Downshift>
// Use this hook approach:
const { getInputProps, getItemProps, isOpen, inputValue, highlightedIndex } = useCombobox({
items,
itemToString: item => item.name,
});
return (
// JSX here
);The hooks provide the same functionality with better TypeScript support, smaller bundle size, and modern React patterns.