Implementation of the `Select all` functionality using react-select package

Introduction

This article will explain the implementation details of the Select all functionality in the multi-select component based on the react-select v5.1.0 package.

Demo

handleChange function

The primary logic of the "Select all" option has been implemented in this function. There can be three main scenarios in the process:

  1. All the elements in the list are selected.
  2. Some of the options in the menu are selected
  3. None of the options is selected.

The first case happens under certain conditions: the current state of the Select all option is unchecked, the length of the selected elements is greater than zero, meanwhile, either the Select all option or all the options in the menu except the Select all option are selected. If these conditions are met, then all the elements in the menu are checked. In the second case, we again check if the length of the selected options is greater than zero, and neither the Select all option nor all of the remaining options in the menu list are selected. If that is the case, then it means only some of the elements are selected. The third case is the condition in which neither all the elements nor some of them are selected which happens when the Select all option is set to the unchecked state. If you look at the code, you will see that only filtered options have been used. It is because the default value of filter input is an empty string which works perfectly in both cases.

 const handleChange = (selected: Option[]) => {
    if (
      selected.length > 0 &&
      !isAllSelected.current &&
      (selected[selected.length - 1].value === selectAllOption.value ||
        JSON.stringify(filteredOptions) ===
          JSON.stringify(selected.sort(comparator)))
    )
      return props.onChange(
        [
          ...(props.value ?? []),
          ...props.options.filter(
            ({ label }: Option) =>
              label.toLowerCase().includes(filterInput?.toLowerCase()) &&
              (props.value ?? []).filter((opt: Option) => opt.label === label)
                .length === 0
          )
        ].sort(comparator)
      );
    else if (
      selected.length > 0 &&
      selected[selected.length - 1].value !== selectAllOption.value &&
      JSON.stringify(selected.sort(comparator)) !==
        JSON.stringify(filteredOptions)
    )
      return props.onChange(selected);
    else
      return props.onChange([
        ...props.value?.filter(
          ({ label }: Option) =>
            !label.toLowerCase().includes(filterInput?.toLowerCase())
        )
      ]);
  };

Custom Option component

By overriding the Option component, checkboxes are added to the options list, moreover, if some of the elements are checked, then the indeterminate state of the Select all option is set to true.

  const Option = (props: any) => (
    <components.Option {...props}>
      {props.value === "*" &&
      !isAllSelected.current &&
      filteredSelectedOptions?.length > 0 ? (
        <input
          key={props.value}
          type="checkbox"
          ref={(input) => {
            if (input) input.indeterminate = true;
          }}
        />
      ) : (
        <input
          key={props.value}
          type="checkbox"
          checked={props.isSelected || isAllSelected.current}
          onChange={() => {}}
        />
      )}
      <label style={{ marginLeft: "5px" }}>{props.label}</label>
    </components.Option>
  );

Custom Input component

This custom input component creates a dotted box around the search input and automatically sets the focus to the search input which is helpful when there are lots of selected options.

  const Input = (props: any) => (
    <>
      {filterInput.length === 0 ? (
        <components.Input autoFocus={props.selectProps.menuIsOpen} {...props}>
          {props.children}
        </components.Input>
      ) : (
        <div style={{ border: "1px dotted gray" }}>
          <components.Input autoFocus={props.selectProps.menuIsOpen} {...props}>
            {props.children}
          </components.Input>
        </div>
      )}
    </>
  );

Custom filter function

This custom function is used to keep the Select all option out of the filtering process.

  const customFilterOption = ({ value, label }: Option, input: string) =>
    (value !== "*" && label.toLowerCase().includes(input.toLowerCase())) ||
    (value === "*" && filteredOptions?.length > 0);

Custom onInputChange function

This function is used to get the filter input value and set it to an empty string on the menu close event.

  const onInputChange = (
    inputValue: string,
    event: { action: InputAction }
  ) => {
    if (event.action === "input-change") setFilterInput(inputValue);
    else if (event.action === "menu-close" && filterInput !== "")
      setFilterInput("");
  };

Custom KeyDown function

This function prevents default action on the space bar button click if the filter input value is not an empty string.

  const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
    if (e.key === " " && !filterInput) e.preventDefault();
  };

Handling state and label value of Select all option

The value of isAllSelected determines the state of the Select all option. And the value of the selectAllLabel determines the value of the Select all option label.

  isAllSelected.current =
      JSON.stringify(filteredSelectedOptions) ===
      JSON.stringify(filteredOptions);

    if (filteredSelectedOptions?.length > 0) {
      if (filteredSelectedOptions?.length === filteredOptions?.length)
        selectAllLabel.current = `All (${filtereds also sus also suOptions.length}) selected`;
      else
        selectAllLabel.current = `${filteredSelectedOptions?.length} / ${filteredOptions.length} selected`;
    } else selectAllLabel.current = "Select all";

    selectAllOption.label = selectAllLabel.current;

What else

This custom multi-select component also provides custom single-select with the checkboxes near options.

Side Notes

If you have a large number of options, you can solve performance issues, by rendering only the items in the list that are currently visible which allows for efficiently rendering lists of any size. To do that you can override the MenuList component by implementing react-window's FixedSizeList. For implementation details, you can look at this stack overflow answer. In the end, this is my first tech blog as a junior frontend developer, so it may not be very well-written. I'd appreciate any feedback.