Search code examples
reactjsreact-hooksdrop-down-menunavbar

React Navbar With Multiple Dropdown Menus


I am newbie to web development and I need to create a navbar which has multiple dropdown menus. As you can see I managed to create it for navItem 'QA'. But I need to create dropdown menus for QA, Users, and Stats.

I would be so greatful if someone could help me as I'm unable to proceed with this problem.
Below you can find the code for Navbar (Navbar.jsx), Dropdown (DropDown.jsx) and Nav-items and dropdown menu items (NavItems.jsx):

Navbar.jsx

```import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { navItems } from '../navItems/NavItems'
import DropDown from '../Dropdown/DropDown'

const Navbar = () => {
  const [dropdown, setDropdown] = useState(false)
  return (
    <>
     <nav className="navbar bg-teal-400 font-sans h-12 flex justify-center items-center fixed w-full">
        <Link to="/" className="text-white text-4xl font-bold">Company Name
        </Link>
        <ul className="nav-items flex list-none justify-center items-center w-3/4">
          {navItems.map((item) => {
            if (item.title === 'QA') {
              return (
                <li 
                  key={item.id} 
                  className='flex items-center text-gray-200 mx-6 font-semibold hover:text-white h-12'
                  onMouseEnter={() => setDropdown(true)} 
                  onMouseLeave={() => setDropdown(false)}
                >  
                <Link to={item.path}>{item.title}</Link>
                {dropdown && <DropDown />}
                </li>
              )
            }

            return (
              <li key={item.id} className='flex items-center text-gray-200 mx-6 font-semibold hover:text-white'>
              <Link to={item.path}>{item.title}</Link>
              </li>
            )
            
          })}
        </ul>
        <button className="border-none text-white font-bold px-4 py-1">Logout</button>
     </nav>
     
    </>
  )
}

export default Navbar

DropDown.jsx

```import React, { useState } from 'react'
import { QADropdown } from '../navItems/NavItems'
import { Link } from 'react-router-dom'

const DropDown = () => {
  const [dropdown, setDropdown] = useState(false);
  return (
    <>
      <ul className={dropdown ? 'hidden':'w-36 absolute list-none text-start top-12 bg-sky-300'} onClick={() => setDropdown(!dropdown)}>
        {QADropdown.map((item) => {
          return (
            <li key={item.id} className='text-white p-2 hover:bg-sky-400 cursor-pointer'>
              <Link 
                to={item.path} 
                className='w-full block'
                onClick={() => setDropdown(false)}
              >
                {item.title} 
              </Link>
            </li>
          )
        })}
      </ul>
      
    </>
  )
}

export default DropDown;

NavItems.jsx

export const navItems = [
  {
    id: 1,
    title: 'MIR',
    path: '/mir',
    cName: 'nav-item'
  },
  {
    id: 2,
    title: 'QA',
    path: '/qa',
    cName: 'nav-item',
  },
  {
    id: 3,
    title: 'Users',
    path: '/users',
    cName: 'nav-item',
  },
  {
    id: 4,
    title: 'Stats',
    path: '/stats',
    cName: 'nav-item'
  },
  {
    id: 5,
    title: 'Tools',
    path: '/tools',
    cName: 'nav-item'
  },
  {
    id: 6,
    title: 'Sheets',
    path: '/sheets',
    cName: 'nav-item'
  },
  {
    id: 7,
    title: 'My Account',
    path: '/personal',
    cName: 'nav-item'
  },
];

//created 3 separate arrays for dropdown menus
export const QADropdown = [
  {
    id: 1,
    title: 'QA Job Selector',
    path: '/qaindex',
    cName: 'submenu-item'
  },
  {
    id: 2,
    title: 'View QA Results',
    path: '/finished_qa_jobs',
    cName: 'submenu-item'
  },
];


export const usersDropdown = [
  {
    id: 1,
    title: 'QA Job Selector',
    path: '/qaindex',
    cName: 'submenu-item'
  },
  {
    id: 2,
    title: 'View QA Results',
    path: '/finished_qa_jobs',
    cName: 'submenu-item'
  },
];



export const statDropdown = [
  {
    id: 1,
    title: 'QA Job Selector',
    path: '/qaindex',
    cName: 'submenu-item'
  },
  {
    id: 2,
    title: 'View QA Results',
    path: '/finished_qa_jobs',
    cName: 'submenu-item'
  },
];

Solution

  • So there's a number of issues going on here. Let's break it down step by step.

    Dropdown state is being duplicated in separate components but not kept in sync despite representing the same value

    There's different ways of solving this. You could remove the state from the DropDown component and update the Navbar state variable to an array and keep track of which dropdown is open/closed, then pass a setter function to the DropDown child so it can update the state on link click. However, a better solution that is more best practice is to move the state down to the lowest level it's needed (DropDown component) and just use it there.

    Navbar.js

    const Navbar = () => {
      return (
        <>
          <nav className="navbar bg-teal-400 font-sans h-12 flex justify-center items-center fixed w-full">
            <Link href="/" className="text-white text-4xl font-bold">Company Name
            </Link>
            <ul className="nav-items flex list-none justify-center items-center w-3/4">
              {navItems.map((item) => {
                if (item.title === 'QA') {
                  return <NavLink key={item.id} item={item} />
                }
    
                return (
                  <li key={item.id} className='flex items-center text-gray-200 mx-6 font-semibold hover:text-white'>
                    <Link href={item.path}>{item.title}</Link>
                  </li>
                )
    
              })}
            </ul>
            <button className="border-none text-white font-bold px-4 py-1">Logout</button>
          </nav>
    
        </>
      )
    }
    
    export default Navbar
    

    NavLink.js

    const NavLink = ({ item }) => {
      const [dropdown, setDropdown] = useState(false);
      return (
        <>
          <li
            className='flex items-center text-gray-200 mx-6 font-semibold hover:text-white h-12'
            onMouseEnter={() => setDropdown(true)}
            onMouseLeave={() => setDropdown(false)}
          >
            <Link href={item.path}>{item.title}</Link>
            <ul className={!dropdown ? 'hidden':'w-36 absolute list-none text-start top-12 bg-sky-300'} onClick={() => setDropdown(!dropdown)}>
              {QADropdown.map((item) => {
                return (
                  <li key={item.id} className='text-white p-2 hover:bg-sky-400 cursor-pointer'>
                    <Link
                      href={item.path}
                      className='w-full block'
                      onClick={() => setDropdown(false)}
                    >
                      {item.title}
                    </Link>
                  </li>
                )
              })}
            </ul>
          </li>
        </>
      )
    }
    
    export default NavLink;
    

    Here I've deleted the dropdown state variable from Navbar.js and moved the <li> element to it's child component. Since it's no longer strictly a dropdown anymore, I've renamed it to NavLink.js to reflect this change. Now that we have the dropdown state in the child component, each dropdown will be able to manage it's own state. Since the <Link> in <li> is expecting variables that were available in Navbar, I've passed item as a prop to the child component.

    Now that we've simplified the state and seen a better way of using it, I'm actually going to remove it. In this case, we don't even need state to handle the logic for us, it's best to use CSS to handle hover states like this. First step is to remove the dropdown state variable and everything referencing it in NavLink. I see you're using Tailwind which is great, now all we need to do is add a group class on the <li> and add group-hover:block to the dropdown <ul>. Finally I've added a hidden class to the <ul> as well so it's hidden by default.

    NavLink.js

    const NavLink = ({ item }) => {
      return (
        <>
          <li
            className='group flex items-center text-gray-200 mx-6 font-semibold hover:text-white h-12'
          >
            <Link href={item.path}>{item.title}</Link>
            <ul className='group-hover:block hidden w-36 absolute list-none text-start top-12 bg-sky-300'>
              {QADropdown.map((item) => {
                return (
                  <li key={item.id} className='text-white p-2 hover:bg-sky-400 cursor-pointer'>
                    <Link
                      href={item.path}
                      className='w-full block'
                    >
                      {item.title}
                    </Link>
                  </li>
                )
              })}
            </ul>
          </li>
        </>
      )
    }
    
    export default NavLink;
    

    Now that this component is looking much simpler and we've eliminated unnecessary state variables, I want to fix the issue of this dropdown only working for the QA items. First I've edited NavItems.js to move the dropdown links as a child of the original item so we can pass those values easily to NavLink.

    NavItems.js

    export const navItems = [
      {
        id: 1,
        title: 'MIR',
        path: '/mir',
        cName: 'nav-item'
      },
      {
        id: 2,
        title: 'QA',
        path: '/qa',
        cName: 'nav-item',
        children: [
          {
            id: 1,
            title: 'QA Job Selector',
            path: '/qaindex',
            cName: 'submenu-item'
          },
          {
            id: 2,
            title: 'View QA Results',
            path: '/finished_qa_jobs',
            cName: 'submenu-item'
          },
        ]
      },
      {
        id: 3,
        title: 'Users',
        path: '/users',
        cName: 'nav-item',
        children: [
          {
            id: 1,
            title: 'QA Job Selector',
            path: '/qaindex',
            cName: 'submenu-item'
          },
          {
            id: 2,
            title: 'View QA Results',
            path: '/finished_qa_jobs',
            cName: 'submenu-item'
          },
        ]
      },
      {
        id: 4,
        title: 'Stats',
        path: '/stats',
        cName: 'nav-item',
        children: [
          {
            id: 1,
            title: 'QA Job Selector',
            path: '/qaindex',
            cName: 'submenu-item'
          },
          {
            id: 2,
            title: 'View QA Results',
            path: '/finished_qa_jobs',
            cName: 'submenu-item'
          },
        ]
      },
      {
        id: 5,
        title: 'Tools',
        path: '/tools',
        cName: 'nav-item'
      },
      {
        id: 6,
        title: 'Sheets',
        path: '/sheets',
        cName: 'nav-item'
      },
      {
        id: 7,
        title: 'My Account',
        path: '/personal',
        cName: 'nav-item'
      },
    ];
    

    Next, in NavLink.js, since I've already passed item as a prop, all I need to do is change QADropdown.map to item.children.map. There's still an issue however, not all nav links have a dropdown so we need to handle that case. Simply add a check for if item.children exists around the dropdown <ul>, if it exists we'll render the dropdown children, if not we don't need to do anything. In the dropdown item.map I've renamed the item variable inside out map function to dropDownItem so it's not confused with the item prop passed to this component. I've also removed the surrounding fragment tag (<></> as it's not needed.

    NavLink.js

    const NavLink = ({item}) => {
      return (
        <li
          className='group flex items-center text-gray-200 mx-6 font-semibold hover:text-white h-12'
        >
          <Link href={item.path}>{item.title}</Link>
          
          {item.children && (
            <ul className='group-hover:block hidden w-36 absolute list-none text-start top-12 bg-sky-300'>
              {item.children.map((dropDownItem) => (
                <li key={dropDownItem.id} className='text-white p-2 hover:bg-sky-400 cursor-pointer'>
                  <Link
                    href={dropDownItem.path}
                    className='w-full block'
                  >
                    {dropDownItem.title}
                  </Link>
                </li>
              ))}
            </ul>
          )}
        </li>
      )
    }
    
    export default NavLink;
    

    Finally, in the Navbar component, there's still some logic which is causing the QA dropdown to be the only one rendered. Let's remove that if statement and just return the NavLink component. We can also remove the other <li> after the if statement as it's not needed, we are already rendering that inside of NavLink. I've also removed the fragment tag here as it's not needed.

    Navbar.js

    const Navbar = () => {
      return (
        <nav className="navbar bg-teal-400 font-sans h-12 flex justify-center items-center fixed w-full">
          <Link href="/" className="text-white text-4xl font-bold">Company Name</Link>
    
          <ul className="nav-items flex list-none justify-center items-center w-3/4">
            {navItems.map((item) => (
              <NavLink key={item.id} item={item} />
            ))}
          </ul>
    
          <button className="border-none text-white font-bold px-4 py-1">Logout</button>
        </nav>
      )
    }
    
    export default Navbar
    

    That's it! We removed duplicated state and refactored it to be used only where it's necessary. Next, we actually removed it altogether since we were able to achieve the same effect with CSS which is much more efficient. Then we restructured the JSON data to allow us to use it more dynamically. Finally we removed hardcoded values and made the components dynamic which allowed them to render what we wanted no matter what the specific data was.

    There's still some optimizations that could be made. For example, the data structure I chose works fine in this case, but for larger datasets you may want to consider "flattening" the data. Another area might be to change the item prop we passed to NavLink to be more explicit than just the general "item" object. Something like id, title, path, children could work or maybe it makes more sense to pass an object for the main link which holds id, title, path and pass an array of children. It's up to you, but as you make components these are the kinds of decisions you'll need to make. It's best to ensure things are clear and explicit throughout to help you avoid bugs and make understanding the flow of the code easier. I recommend reading the React Docs to help understand React and it's nuances better. Good luck!