Search code examples
reactjsevent-handlingstyled-componentsreact-hooks

How to target a specific item to toggleClick on using React Hooks?


I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.

The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.

Heres my component.

const NavBar = props => {
 const [links, setLinks] = useState(null);
 const [notFound, setNotFound] = useState(false);
 const [isOpen, setIsOpen] = useState(false);

 const fetchLinks = () => {
   if (props.prismicCtx) {
     // We are using the function to get a document by its uid
     const data = props.prismicCtx.api.query([
       Prismic.Predicates.at('document.tags', [`${config.source}`]),
       Prismic.Predicates.at('document.type', 'navbar'),
     ]);
     data.then(res => {
       const navlinks = res.results[0].data.nav;
       setLinks(navlinks);
     });
   }
   return null;
 };

 const checkForLinks = () => {
   if (props.prismicCtx) {
     fetchLinks(props);
   } else {
     setNotFound(true);
   }
 };

 useEffect(() => {
   checkForLinks();
 });

 const handleDropdownClick = e => {
   e.preventDefault();
   setIsOpen(!isOpen);
 };

 if (links) {
   const linkname = links.map(item => {
     // Check to see if NavItem contains Dropdown Children
     return item.items.length > 1 ? (
       <Fragment>
         <StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
           {item.primary.label[0].text}
         </StyledNavBar.NavLink>
         <Dropdown toggleOpen={isOpen}>
           {item.items.map(subitem => {
             return (
               <StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
                 <span>{subitem.sub_nav_link_label[0].text}</span>
               </StyledNavBar.NavLink>
             );
           })}
         </Dropdown>
       </Fragment>
     ) : (
       <StyledNavBar.NavLink href={item.primary.link.url}>
         {item.primary.label[0].text}
       </StyledNavBar.NavLink>
     );
   });
   // Render
   return (
     <StyledNavBar>
       <StyledNavBar.NavContainer wide>
         <StyledNavBar.NavWrapper row center>
           <Logo />
           {linkname}
         </StyledNavBar.NavWrapper>
       </StyledNavBar.NavContainer>
     </StyledNavBar>
   );
 }
 if (notFound) {
   return <NotFound />;
 }
 return <h2>Loading Nav</h2>;
};

export default NavBar;

Solution

  • Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:

    const [isOpen, setIsOpen] = useState({});

    const handleDropdownClick = e => {
        e.preventDefault();
        const currentID = e.currentTarget.id;
        const newIsOpenState = isOpen[id] = !isOpen[id];
        setIsOpen(newIsOpenState);
    };
    

    And finally in your HTML:

    const linkname = links.map((item, index) => {
        // Check to see if NavItem contains Dropdown Children
        return item.items.length > 1 ? (
            <Fragment>
                <StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
                   {item.primary.label[0].text}
                </StyledNavBar.NavLink>
                <Dropdown toggleOpen={isOpen[index]}>
    
        // ... rest of your component
    

    Note the new index variable in the .map function, which is used to identify which menu item you are clicking.

    UPDATE:

    One point that I was missing was the initialization, as mention in the other answer by @MattYao. Inside your load data, do this:

    data.then(res => {
        const navlinks = res.results[0].data.nav;
        setLinks(navlinks);
        setIsOpen(navlinks.map((link, index) => {index: false}));
    });
    

    Not related to your question, but you may want to consider skipping effects and including a key to your .map