3
\$\begingroup\$

Rate my React dropdown that auto closes when the user clicks outside of it. This is an example in Typescript that I did for practice.

import React, {useEffect, useRef, useState} from "react";
import "./Dropdown.css";
import classNames from "classnames";

interface DropdownProps {
  label: string;
}

const Dropdown: React.FC<DropdownProps> = ({children, label}) => {
  const [showDropdown, setShowDropdown] = useState<boolean>(false);
  const dropdownContents = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClick = (event: MouseEvent) => {
      if (
        dropdownContents.current &&
        event.target instanceof Node &&
        !dropdownContents.current.contains(event.target)) {
        setShowDropdown(false);
      }
    };
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
  }, []);

  return (
    <div ref={dropdownContents} className="dropdown-wrapper">
      <button className="dropdown-btn" onClick={() => setShowDropdown((show) => !show)}>{label}</button>
      <div className={classNames(["dropdown-contents"], {visible: showDropdown})}>
        {children}
      </div>
    </div>
  )
}
export default Dropdown;
.dropdown-wrapper {
  position: relative;
}

.dropdown-btn {
  border-radius: 0;
  border: 1px solid black;
  padding: 0.5rem 1rem;
  background: #fff;
  font-weight: bold;
}

.dropdown-btn:focus {
  outline: 1px solid blue;
}

.dropdown-btn:hover {
  background: #efefef;
}

.dropdown-btn:active {
  background: #bbb;
}

.dropdown-contents {
  position: absolute;
  left: 0;
  top: 100%;
  display: none;
  -webkit-box-shadow: 5px 5px 15px 5px #000000;
  box-shadow: 5px 5px 15px 5px #a8a8a8;
  padding: 0.5rem;
}

.visible {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

\$\endgroup\$
3
  • \$\begingroup\$ Welcome to Code Review! The module in the snippet doesn't work so well. You can likely enable Babel and convert the import statements to an assignment statements for the sake of demonstration - e.g. import React, {useEffect, useRef, useState} from "react"; to const {useEffect, useRef, useState} - React;. See this MSO post for more information. \$\endgroup\$ Commented Mar 11, 2022 at 5:25
  • \$\begingroup\$ Why is your ref attached to the outer div rather than the inner one? \$\endgroup\$ Commented Mar 11, 2022 at 22:13
  • \$\begingroup\$ @NickBailey If it wasn't the event listener for clicking would think clicking on the button is outside of clicking on the dropdown, and the dropdown would always close right after opening. It was a bug I found while developing this component. \$\endgroup\$ Commented Mar 13, 2022 at 2:39

1 Answer 1

4
\$\begingroup\$

Good job in getting something handcrafted to work; must have been a fun learning experience!

With a production / longer term view on things, here's my review on what you've done:

  1. The "click outside" implementation was probably a good learning experience. But, in production I wouldn't reinvent such a common utility as this. Instead, I'd use something like useOutSideClick. It makes the code more focused and elegant to read.
  2. Destructure the import for FC (you have done for everything else from react), rather than the inline React.FC. It's consistent and it may even help treeshaking.
  3. Take advantage of TypeScript inference for the useState usage; no need to explictly set this to boolean.
  4. There's a conspicuous lack of state management with the children of the dropdown component - is that left to the consumer? E.g. maintaining a "selected" item state, etc. Perhaps you felt that was out of scope for this review...
  5. I appreciate you're probably not after this advice, having handcrafted this component yourself: but for such a common UI element, I'd recommend an off the shelf dropdown component, such as React Select.
  6. Consider a CSS-in-JS solution, to avoid the global CSS styles. Something like Styled Components. This might be a bit out of scope here though.

To conclude, here's my refactored code based on some of the above points:

import React, { useRef, useState, FC } from "react";
import "./Dropdown.css";
import classNames from "classnames";
import { useOutsideClick } from "rooks";

interface DropdownProps {
  label: string;
}

const Dropdown: FC<DropdownProps> = ({ children, label }) => {
  const [showDropdown, setShowDropdown] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useOutsideClick(ref, () => setShowDropdown(false));

  return (
    <div ref={ref} className="dropdown-wrapper">
      <button
        className="dropdown-btn"
        onClick={() => setShowDropdown((show) => !show)}
      >
        {label}
      </button>
      <div
        className={classNames(["dropdown-contents"], { visible: showDropdown })}
      >
        {children}
      </div>
    </div>
  );
};

export default Dropdown;

Good luck and have fun with your component!

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.