Build a search bar with filter in React - Part 2

Build a search bar with filter in React - Part 2

We'll bring some improvements to our advanced search bar.

The final rendering can be tested on StackBlitz.

Open in StackBlitz

The context

We've built in this article a search bar with select and filter in React. Now here, we'll add three features to our component. We'll:

  1. Allow renaming the "All" keyword

  2. Allow uncategorised data (data that returns undefined or null after applying the groupBy callback)

  3. Extract the component main logic and put it in a custom hook (refactoring): useSearchbarWithSelectAndFilter (awesome as name, isn't it ? I know, Thank you)

The solution

Allow user to rename the "All" keyword

To do it,

  1. We add a property "allLabel" to the component props as an optional string with a default value of "All" or "Tout" (in french). So

    • types.ts

        // other types
        allLabel?: string;
      
    • SearchbarWithSelectAndFilter.tsx

        {
            // other props
            allLabel = "All",
        }
      
  2. In the code section where we displayed by default "All", we replace it with this code

     <MenuItem value={DEFAULT_SELECT_VALUE}>
         {allLabel ?? "All"}
     </MenuItem>
    

Allow user to have uncategorised data (data which returns undefined or null after applying the groupBy callback)

Currently, our filter list only data on which the callback groupBy returns either a number, string or boolean. But what happens if the callback returns undefined | null which means may be, the value at this time is unknown. So we'll configure our component to put this kind of data in the category Uncategorised. And to do it, let's follow the steps in the next image:

Searchbar with select and filter new architecture: uncaetgorised

So, let's start

  1. Add uncategorised , uncategorisedLabel to the component props and extend the return type of groupBy to undefined | null .

    • types.ts

        export interface SearchbarWithSelectAndFilterProps<T extends unknown> {
            // other types
      
            groupBy: (value: T) => string | number | boolean | null | undefined;
      
            /**
             * Refer to every data with "undefined" or "null" as value      after apply groupBy callback
             */
            uncategorised?: boolean;
      
            /**
             * Label displayed in dropdown to represent "unCategorised" value
             */
            uncategorisedLabel?: string;
      
            // other types
        }
      
    • SearchbarWithSelectAndFilter.tsx

        ({
            // other props
      
            uncategorised = true,
            uncategorisedLabel = "Uncategorised",
      
            // other props
        })
      
  2. Add UNCATEGORISED_SELECT_VALUE to the dropdown list

    When we retrieve the dropdown values, if the uncategorised prop is true, we'll check if the returned data after applying groupBy is undefined or null. In this case instead of adding undefined or null to the dropdown values we add UNCATEGORISED_SELECT_VALUE . So let's replace the old itemsGroup with this new code :

       // UNCATEGORISED_SELECT_VALUE = "ANY_KIND_OF_VALUE_YOU_WANT"
    
       // get the list of items for drowpdown
       let itemsGroup = useMemo(() => {
         let itemsAsSet = new Set();
         searchData.forEach((data) => {
           let option = groupBy(data) ?? undefined;
           if (uncategorised) {
             option = option == undefined ? UNCATEGORISED_SELECT_VALUE : option;
             itemsAsSet.add(option);
           } else if (!uncategorised && option != undefined) {
             itemsAsSet.add(option);
           }
         });
         return Array.from(itemsAsSet);
       }, [searchData, groupBy]);
    
  3. Update the list of data provided to the autocomplete

    We bring a little bit of change to our useEffect.

    
         // update list of options provided to autocomplete
         useEffect(() => {
           const updateSearchList = new Map(searchList)
           if (!updateSearchList.has(selectedItem)) {
               if (selectedItem == DEFAULT_SELECT_VALUE) {
                   updateSearchList.set(DEFAULT_SELECT_VALUE, searchData)
               } else {
                   updateSearchList.set(
                       selectedItem,
                       searchData.filter((v) =>  {
                           const option = groupBy(v)
                           if (selectedItem == UNCATEGORISED_SELECT_VALUE) {
                               return option == undefined || option == null 
                           } else return option == selectedItem
                       })
                   )
               }
               setSearchList(updateSearchList)
           }
       }, [selectedItem, searchData])
    

    Notice that we don't check if uncategorised is true or not because we assume that if UNCATEGORISED_SELECT_VALUE is among dropdown values which means that uncategorised is true so undefined or null are allowed.

Refactoring the component logic by putting it into a custom hook

At the end of the first article of this series, I asked how we can extract the logic from the component so that it can be reused with MUI as well as other UI libraries like Mantine or Ant Design. To answer this question let's create a custom hook that'll contain all the (independent) code of the component:

  • state

  • useEffect, useMemo

  • functions utilities or event handler

  1. Move component logic to the hook

    A hook is like a usual javascript function that can hold another hook like default hooks from React (state, useEffect, ...). So let's create a new file called useSearchbarWithSelectAndFilter.ts in the same folder as our component. Here we'll create a hook like this:

    ```typescript export const useSearchbarWithSelectAndFilter = (/ parameters /) => { /**

    * hooks
    */
    
  /**
   * functions
   */

  return {
    /* returned values */
  };
};

export default useSearchbarWithSelectAndFilter
```
  1. Dropdown values and autocomplete list as states

    We'll add the states that hold the dropdown values and the list for the autocomplete

    ```typescript export const useSearchbarWithSelectAndFilter = (/ parameters /) => { /**

    * hooks
    */
    

    const [selectedItem, setSelectedItem] = useState(DEFAULT_SELECT_VALUE); const [searchList, setSearchList] = useState(

     new Map<ReturnType<typeof groupBy>, T[]>()
    

    );

  /**
   * functions
   */
  // will set the selected dropdown's item
  const handleSelectedItemChange = (event: any) => {
    setSelectedItem(event.target.value);
  };

  return {
    /* returned values */
  };
};

export default useSearchbarWithSelectAndFilter
```

So here, our hook needs the `groupBy` callback and the type of each element in the list provided to the autocomplete. We'll add so a generic type to the hook and then pass the `searchData` and `groupBy` callback to the hook parameters.

```typescript
export const useSearchbarWithSelectAndFilter = <T>(
    searchData: SearchbarWithSelectAndFilterProps<T>["searchData"] = [],
    groupBy: SearchbarWithSelectAndFilterProps<T>["groupBy"]
) => {
  /**
   * hooks
   */
const [selectedItem, setSelectedItem] = useState(DEFAULT_SELECT_VALUE);
const [searchList, setSearchList] = useState(
    new Map<ReturnType<typeof groupBy>, T[]>()
);


  /**
   * functions
   */
  // will set the selected dropdown's item
  const handleSelectedItemChange = (event: any) => {
    setSelectedItem(event.target.value);
  };

  return {
    /* returned values */
  };
};

export default useSearchbarWithSelectAndFilter
```

`searchData` and `groupBy` are the same types as those used by our component so we use type indexing to retrieve these types from **SearchbarWithSelectAndFilterProps** (type of our component props)
  1. Handling autocomplete list filtering and dropdown values enumeration

    ```typescript export const useSearchbarWithSelectAndFilter = (

     searchData: SearchbarWithSelectAndFilterProps<T>["searchData"] = [],
     groupBy: SearchbarWithSelectAndFilterProps<T>["groupBy"],
     options?: {
         uncategorised?: SearchbarWithSelectAndFilterProps<T>["uncategorised"]
     }
    

    ) => { /**

    * hooks
    */
    

    const [selectedItem, setSelectedItem] = useState(DEFAULT_SELECT_VALUE); const [searchList, setSearchList] = useState(

     new Map<ReturnType<typeof groupBy>, T[]>()
    

    );

    /**

    * functions
    */
    

    // will set the selected dropdown's item const handleSelectedItemChange = (event: any) => {

     setSelectedItem(event.target.value);
    

    };

     // get the list of items for drowpdown
    

    let itemsGroup = useMemo(() => {

     let itemsAsSet = new Set();
     searchData.forEach((data) => {
       let option = groupBy(data) ?? undefined;
       if (options?.uncategorised) {
         option = option == undefined ? UNCATEGORISED_SELECT_VALUE : option;
         itemsAsSet.add(option);
       } else if (!options?.uncategorised && option != undefined) {
         itemsAsSet.add(option);
       }
     });
     return Array.from(itemsAsSet);
    

    }, [searchData, groupBy]);

    // update list of options provided to autocomplete
    useEffect(() => {
      const updateSearchList = new Map(searchList)
      if (!updateSearchList.has(selectedItem)) {
          if (selectedItem == DEFAULT_SELECT_VALUE) {
              updateSearchList.set(DEFAULT_SELECT_VALUE, searchData)
          } else {
              updateSearchList.set(
                  selectedItem,
                  searchData.filter((v) =>  {
                      const option = groupBy(v)
                      if (selectedItem == UNCATEGORISED_SELECT_VALUE) {
                          return option == undefined || option == null 
                      } else return option == selectedItem
                  })
              )
          }
          setSearchList(updateSearchList)
      }
  }, [selectedItem, searchData])

  return {
    /* returned values */
  };
};

export default useSearchbarWithSelectAndFilter
```

So here we add optional props as an object with property uncategorised. (*I try to do it like what I see in packages: extra properties are optional and put in the options object*)
  1. Last but not the least: the returned values

    Our component needs the items and selected item for dropdown and the grouped lists for the autocomplete so let's return them in the hook:

     return {
         filter: { selectedItem, setSelectedItem, handleSelectedItemChange },
         autocomplete: {searchList, setSearchList},
         itemsGroup,
       };    
     };
    

So yes we complete the three tasks with a reusable hook.

Conclusion

And again we are at the end of this article, we add some fancy features to our component and I hope that you enjoy this series. So let me know if you have any kind of suggestions