DEV Community

Zee Yass
Zee Yass

Posted on • Updated on

Handle Click Outside React Component.

In this tutorial we are going to make a dropdown component, creating the drop down functionality, and then handling the click outside, it's a perfect use case on how to handle click outside a react comoponent, let's do that 🔥

first create a dropdown button, for this am using a free element from the taillwind ui library kit:

export function DropDown() {
    return(
        <div className="relative inline-block text-left">
                <div>
                    <button type="button" className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" id="menu-button" aria-expanded="true" aria-haspopup="true">
                        Options
                        <svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                            <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
                        </svg>
                    </button>
                </div>
                <div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
                    <div className="py-1" role="none">
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-0">Account settings</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-1">Support</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-2">License</a>
                        <form method="POST" action="#" role="none">
                            <button type="submit" className="text-gray-700 block w-full text-left px-4 py-2 text-sm" role="menuitem" id="menu-item-3">Sign out</button>
                        </form>
                    </div>
                </div>
            </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

dropdown no effect

till now the button has no dropdown effect, let's add some state to handle this.

import { useState } from "react";
export function DropDown() {
     const [dropDownOpen, setDropDownOpen] = useState(false)
     const toggleDropDown = () => setDropDownOpen(!dropDownOpen)
      ...
    {/* allow the button to change state */}
     <button 
      ...
     onClick={toggleDropDown}>
      ...
    {/* show the options area based on the dropdown state */}
    { dropDownOpen &&
                <div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
                    <div className="py-1" role="none">
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-0">Account settings</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-1">Support</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-2">License</a>
                        <form method="POST" action="#" role="none">
                            <button type="submit" className="text-gray-700 block w-full text-left px-4 py-2 text-sm" role="menuitem" id="menu-item-3">Sign out</button>
                        </form>
                    </div>
                </div>}
    ...
Enter fullscreen mode Exit fullscreen mode

drop down working

Cool, the dropdown works fine now, but nothing happen when we click outside, let's use the useEffect() and useRef() react hooks to get the desired result.

import { useEffect, useRef, useState } from "react";
export function DropDown() {
...
const dropDownRef = useRef<HTMLDivElement>(null);

//change dropdown state to false when clicking outside.
useEffect(() => {
        function handler(event:any) {
            if(!dropDownRef.current?.contains(event.target)) {
                setDropDownOpen(false);
            }
        }
        window.addEventListener('click', handler)
        return () => window.removeEventListener('click', handler)
    }, []);
...
return(
        <div 
        ref={dropDownRef}
        ...>
...
Enter fullscreen mode Exit fullscreen mode

click outside working

you see how this is easy, before finishing the tutorial why not adding some transition effect to the dropdown area, for this let's use the Transition component from headles ui.
npm install @headlessui/react

full code of the component.

import { Transition } from "@headlessui/react";
import { useEffect, useRef, useState } from "react";

export function DropDown() {
    const [dropDownOpen, setDropDownOpen] = useState(false);
    const toggleDropDown = () => setDropDownOpen(!dropDownOpen)
    const dropDownRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        function handler(event: any) {
            if (!dropDownRef.current?.contains(event.target)) {
                setDropDownOpen(false);
            }
        }
        window.addEventListener('click', handler)
        return () => window.removeEventListener('click', handler)
    }, []);

    return (
        <div
            ref={dropDownRef}
            className="relative inline-block text-left">
            <div>
                <button
                    type="button"
                    className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" id="menu-button" aria-expanded="true" aria-haspopup="true"
                    onClick={toggleDropDown}>
                    Options
                    <svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                        <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
                    </svg>
                </button>
            </div>
            <Transition
                show={dropDownOpen}
                enter="transition ease-out duration-100"
                enterFrom="transform opacity-0 scale-95"
                enterTo="transform opacity-100 scale-100"
                leave="transform opacity-100 scale-100"
                leaveFrom="transform opacity-100 scale-100"
                leaveTo="transform opacity-0 scale-95">
                <div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
                    <div className="py-1" role="none">
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-0">Account settings</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-1">Support</a>
                        <a href="#" className="text-gray-700 block px-4 py-2 text-sm" role="menuitem" id="menu-item-2">License</a>
                        <form method="POST" action="#" role="none">
                            <button type="submit" className="text-gray-700 block w-full text-left px-4 py-2 text-sm" role="menuitem" id="menu-item-3">Sign out</button>
                        </form>
                    </div>
                </div>
            </Transition>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

to learn more i am sharing my #buildinpublic journey on twitter.
hope this was helpful, stay tunned for my upcoming serie on scaffolding authentication with Remix.

Top comments (0)