Fluent UI | React | Controlled multi-select Dropdown - Selected Keys

Posted by Andrew Wilson on Monday, February 13, 2023

Problem Space

I ran into a problem the other day working with the Fluent UI React component Controlled Multi-select Dropdown. As context, here is my redacted code and explanation:


import { Dropdown, IDropdownOption } from 'office-ui-fabric-react'; // office-ui-fabric-react@7.204.0

export interface State {
	itemList: IDropdownOption[];
	selectedItems: string[];
}

export default class DropDownExample extends React.Component<any, State> {
	constructor(context) {
		super(context);
		this.state = {
			itemList: [{ key: 'None', text: 'None' }],
			selectedItems: []
		}
		this.GetItems();
		this.LoadPreselection();
	}
		
	private async GetItems(){
		// ... load items into itemList
	}

	private async LoadPreselection(){
		// ... load pre-selected items into selectedItems
	}

	public onItemChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {

		if (item) {
      		let tempItemsArray = selectedKeys;
      		const index = tempItemsArray.indexOf(item.key, 0); // Obtain item index if exists - returns -1 if it does not

      		if (index > -1) {
      		   tempItemsArray.splice(index, 1); // If the item exists, remove the item from the existing array through the splice function
			} else {
      		  tempItemsArray.push(item.key); // If the item does not exist, add the item to the existing array
      		}

      		setSelectedKeys(tempItemsArray); // Update State
    	}

	}

	render() {
		return (

			...

			<Dropdown
				placeholder="[Select Item]"
				disabled={this.state.itemList.length === 0}
				label='Items'
				id='items'
				responsiveMode={1}
				multiSelect
				options={this.state.itemList}
				selectedKeys={this.state.selectedItems}
				onChange={this.onItemChange.bind(this)}
			/>

			...

		);
	}
}

I had a list of items that I wanted to load into the Multi-select control from an API call. After obtaining the list of items, I wanted to preload any previously selected items.

At this point there were no problems…. until I had written my onchange method. For some reason I was able to manage the list of selected keys absolutely fine, but there were no visible changes to the UI selection when clicking.

For example:

  1. Open drop down.
  2. Select an option.
    • onChange event triggered (state managed).
    • Selection in dropdown hasn’t changed (Item UI does not confirm selection OR un-selection)

As you can imagine, this caused some confusion. The SelectedKeys Array has the correct values but the control is not confirming what I am seeing behind the scenes.

This is until the penny dropped when reviewing example solutions.

Solution

The Dropdown control is treating the selectedKeys as an IMutable "(Once set, it cannot be changed)". This completely changes the onChanged method solution.

Rather than Push to the existing array, we need to use the Spread syntax. The spread syntax will add the new item along with the already existing items into a new array that can be used by the control. Such as:


...this.state.selectedItems, item.key as string

Rather than using Splice to remove an element from an existing array, we need to use the Filter method. The filter method creates a new array with all the elements that pass the logic test supplied, in this case bring everything over that doesn’t match the selected item. Such as:


this.state.selectedItems.filter(key => key !== item.key)

Combined, my onChange method now takes on the following form and operates as expected:


public onItemChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {

		if (item) {
			this.setState({
				selectedItems:
					item.selected
						? [...this.state.selectedItems, item.key as string]
						: this.state.selectedItems.filter(key => key !== item.key),
			});
		}

	}