I have a component with a custom dropdown and the function for closing the dropdown on clicking anywhere outside the component works as it should (see below):
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsDropdownOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => { //cleanup function
document.removeEventListener("mousedown", handleClickOutside);
};
});
const toggleDropDown = () => {
setIsDropdownOpen(!isDropdownOpen)
}
return (
<div onClick={toggleDropDown}> click here
{isDropdownOpen && (
<div ref={dropdownRef}>
<div onClick={handleOption2}>Option 1</div>
<div onClick={handleOption2}>Option 2</div>
</div>
)}
</div>
)
When I dynamically create the component for each item in an array, I use an array of refs instead and I write the outside click function as shown below:
const [allDropdownOpen, setAllDropdownOpen] = useState([]);
const dropdownRefs = useRef([]);
// set false dropdown states for all items in array (the array is fetched through an API call)
useEffect(() => {
if(fetchedArray) {
let arr = [];
fetchedArray.forEach(() => arr.push(false));
setAllDropdownOpen(arr)
}
}, [fetchedArray])
useEffect(() => {
let arr = [];
if(fetchedArray) {
fetchedArray.forEach(() => arr.push(false)); //create false states for all items in array
}
const handleClickOutside = (e) => {
if(dropdownRefs.current.some(ref => ref && !ref.contains(e.target))) {
setAllDropdownOpen(arr) //close all dropdowns
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => { //cleanup function
document.removeEventListener("mousedown", handleClickOutside);
};
});
const toggleDropDown = (index) => {
//change dropdown state for selected item
const arr = [...allDropdownOpen];
arr[index] = !arr[index];
setAllDropdownOpen([...arr]);
}
return (
<div>
{fetchedArray.map((data, i) => (
<div
onClick={() => toggleDropDown(i)}
ref={el => (dropdownRefs.current[i] = el)}
>
click here
{allDropdownOpen[i] && (
<div>
<div onClick={handleOption2}>Option 1</div>
<div onClick={handleOption2}>Option 2</div>
</div>
)}
</div>
)}
</div>
)
Although this works and the dropdowns close, the issue I'm facing now is that when I click an option in the dropdown list "Option 1" or "Option 2", it doesn't execute handleOption1
or handleOption2
. It just closes all dropdowns.
I've confirmed that the options are in fact clickable and they execute when I remove the useEffect
hook with the handleOutsideClick
function so I know the error is from there.
How do I avoid this and correctly target the dropdown of the clicked array item?
UPDATE : I FINALLY FOUND MY ACTUAL ERROR!
I was attaching the ref
to the wrong element! It was attached to the parent div
with the onClick
property when it should have been attached to the dropdown itself (as it was in the code for a single component)🤦♀️:
So this part:
return (
<div>
{fetchedArray.map((data, i) => (
<div
onClick={() => toggleDropDown(i)}
ref={el => (dropdownRefs.current[i] = el)}
>
click here
{allDropdownOpen[i] && (
<div>
<div onClick={handleOption2}>Option 1</div>
<div onClick={handleOption2}>Option 2</div>
</div>
)}
</div>
)}
</div>
)}
should have been:
return (
<div>
{fetchedArray.map((data, i) => (
<div
onClick={() => toggleDropDown(i)}
>
click here
{allDropdownOpen[i] && (
<div ref={el => (dropdownRefs.current[i] = el)}>
<div onClick={handleOption2}>Option 1</div>
<div onClick={handleOption2}>Option 2</div>
</div>
)}
</div>
)}
</div>
)}
Such a tiny mistake and it cost me all of my braincells🤦♀️🤦♀️
. .
PREVIOUS ANSWER
After an endless night of searching for my error, I decided to go this route:
currentDropdownIndex
state, const Wrapper = () => {
const [allDropdownOpen, setAllDropdownOpen] = useState([]);
const [currentDropdownIndex, setCurrentDropdownIndex] = useState(null);
const dropdownRefs = useRef([]);
// set false dropdown states for all items in array (the array is fetched through an API call)
useEffect(() => {
if(fetchedArray) {
let arr = [];
fetchedArray.forEach(() => arr.push(false));
setAllDropdownOpen(arr)
}
}, [fetchedArray])
//target ref of clicked dropdown using currentDropdownIndex
useEffect(() => {
let arr = [];
if(fetchedArray) {
fetchedArray.forEach(() => arr.push(false));
}
const handleClickOutside = (e) => {
if(dropdownRefs.current[currentDropdownIndex] &&
!dropdownRefs.current[currentDropdownIndex].contains(e.target))) {
setAllDropdownOpen(arr)
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
});
//change dropdown state for selected item
const toggleDropDown = (index) => {
const arr = [...allDropdownOpen];
arr[index] = !arr[index];
setAllDropdownOpen([...arr]);
setCurrentDropdownIndex(index); //set dropdown index to clicked array item
}
return (
<div>
{fetchedArray.map((data, i) => (
<div
onClick={() => toggleDropDown(i)}
ref={el => (dropdownRefs.current[i] = el)}
>
click here
{allDropdownOpen[i] && (
<div ref={dropdownRef}>
<div onClick={handleOption2}>Option 1</div>
<div onClick={handleOption2}>Option 2</div>
</div>
)}
</div>
)}
</div>
)}
And yeah, it worked.