Search code examples
javascriptreactjsreact-hooksdropdownuse-ref

Clicking option in a custom dropdown [React component with outside click function & array of refs]


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?


Solution

  • 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:

    1. initialise a currentDropdownIndex state,
    2. set its value as the index of the item whenever its dropdown is clicked,
    3. use the index to target the ref individually for the item
        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.