The final rendering can be tested on StackBlitz.
![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg align="left")
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:
-
Allow renaming the "All" keyword
-
Allow uncategorised data (data that returns undefined or null after applying the
groupBy
callback) -
Extract the component main logic and put it in a custom hook (refactoring): useSearchbarWithSelectAndFilter (awesome as name, isn't it ? I know, Thank you)
To do it,
-
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", }
-
-
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1672122036914/53328bde-a504-449e-bd58-d3006bc27ec1.png align="center")
So, let's start
-
Add
uncategorised
,uncategorisedLabel
to the component props and extend the return type ofgroupBy
toundefined | 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 })
-
-
Add
UNCATEGORISED_SELECT_VALUE
to the dropdown listWhen 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 addUNCATEGORISED_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]);
-
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.
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
-
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:
export const useSearchbarWithSelectAndFilter = (/* parameters */) => { /** * hooks */ /** * functions */ return { /* returned values */ }; }; export default useSearchbarWithSelectAndFilter
-
Dropdown values and autocomplete list as states
We'll add the states that hold the dropdown values and the list for the autocomplete
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 thesearchData
andgroupBy
callback to the hook parameters.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
andgroupBy
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) -
Handling autocomplete list filtering and dropdown values enumeration
export const useSearchbarWithSelectAndFilter = <T>( 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)
-
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.
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