Hai folks. In this blog we are going to build a custom select component using the compound components pattern. If you are not familiar with what are compound components pattern? and how does the compound components pattern looks like?. Please check out my blog on compound components.
I hope you have used the native HTML Select Component. In the native select component, You can see there are two components : select and option. The both should be used in order to use the native select component.
<select>
<option>One</option>
<option>Two</option>
<option>Three</option>
</select>
The select component is the outer container component. the option are to define options for the select component. The native select component uses the compound components patterns. we are going to use the same pattern for our Select component.
First let's build the select. The Select component is the container components that maintains the state. There are two main states required to build the select component.
- selectedOption (value of the currently selected option).
- showDropdown (boolean value to show or hide the dropdown list).
import React, { ReactNode, useState, useRef } from "react";
import useOnClickOutside from "../../hooks/useOnClickOutside";
const Select: React.FC<{
children: ReactNode | ReactNode[];
defaultValue?: string;
placeholder?: string;
}> = ({ children, defaultValue, placeholder }) => {
const [selectedOption, setSelectedOption] = useState(defaultValue || "");
const [showDropdown, setShowDropdown] = useState(false);
const showDropdownHandler = () => setShowDropdown(!showDropdown);
const selectPlaceholder = placeholder || "Choose an option";
const clickOutsideHandler = () => setShowDropdown(false);
// custom hook to detect the click on the outside
useOnClickOutside(selectContainerRef, clickOutsideHandler);
const updateSelectedOption = (option: string) => {
setSelectedOption(option);
setShowDropdown(false);
};
return (
<div className="select-container" ref={selectContainerRef}>
<div
className={showDropdown ? "selected-text active" : "selected-text"}
onClick={showDropdownHandler}
>
{selectedOption.length > 0 ? selectedOption : selectPlaceholder}
</div>
<ul
className={
showDropdown
? "select-options show-dropdown-options"
: "select-options hide-dropdown-options"
}
>
{children}
</ul>
</div>
);
};
export default Select;
I am using a custom hook called useOnClickOutside. This hook is like a listener that accepts a ref and a callback function. the callback function will be called whenever the click event occurs outside the specified ref. I used this custom hook here to hide the dropdown list whenever the user clicks outside the select component. Now we had finished building the outer component (Select). Next step is to build the Option component.
import React, { ReactNode } from "react";
const Option: React.FC<{
children: ReactNode | ReactNode[];
value: string;
}> = ({ children, value }) => {
return (
<li className="select-option">
{children}
</li>
);
};
export default Option;
The Option component is now built. Now the difficult part, We need to use the updateSelectedOption function which is present in the select component in the Option component. how are we going to share the function or state between Select and Option Components without passing through props?
Chill, React provides a way to share the data without passing through props, This is where the React Contexts comes into play. If you are not familiar with React context, please refer https://reactjs.org/docs/context.html.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Now Let's write context for select component. We will be sharing two values : selectedOption & updateSelectedOption.
import { createContext, useContext } from "react";
const SelectContext = createContext<{
selectedOption: string;
changeSelectedOption: (option: string) => void;
}>({
selectedOption: "",
changeSelectedOption: (option: string) => {}
});
const useSelectContext = () => {
const context = useContext(SelectContext);
if (!context) {
throw new Error("Error in creating the context");
}
return context;
};
export { useSelectContext, SelectContext };
We had create the select context and useSelectContext is a custom hook for using the context. Now we need to provide the values to the context. We can provide values to the context using the SelectContext.Provider element.
// Select component
<SelectContext.Provider
value={{ selectedOption, changeSelectedOption: updateSelectedOption }}
>
<div className="select-container" ref={selectContainerRef}>
...
...
</div>
</SelectContext.Provider>
Now we had provided the values to the Context. Next step is to use the provided values in the Option component. We can make use of useSelectContext to get the values from the context.
import React, { ReactNode } from "react";
import { useSelectContext } from "./selectContext";
const Option: React.FC<{
children: ReactNode | ReactNode[];
value: string;
}> = ({ children, value }) => {
const { changeSelectedOption } = useSelectContext();
return (
<li className="select-option" onClick={() => changeSelectedOption(value)}>
{children}
</li>
);
};
export default Option;
We had now connected the Option & Select component. Clicking on any of the option will trigger the changeSelectedOption function which will update the selectedOption state. We can also use selectedOption value in the context in the Option component for highlighting the selected option.
<Select>
<Option value="one">One</Option>
<Option value="two">Two</Option>
<Option value="three">Three</Option>
<Option value="four">Four</Option>
</Select>
Cheers folks. We had built the Select component which works the same way like the native select component.
You can get the complete code and check out the demo in the codesandbox. Happy coding friends.
Top comments (3)
This is just an example to explain the way of using compound components pattern. So It does not handle all the edge cases. If you want to get the selected value. May be you can declare the prop onSelect callback function in Select component. You can call the onSelect whenever the option value changes So that you can get the selected value of the component
How can make select multiple ?
How do I use the selected value of this custom selection component?