15

I am trying to use react hooks to determine if a user has clicked outside an element. I am using useRef to get a reference to the element.

Can anyone see how to fix this. I am getting the following errors and following answers from here.

Property 'contains' does not exist on type 'RefObject'

This error above seems to be a typescript issue.

There is a code sandbox here with a different error.

In both cases it isn't working.

import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';

const Menu = () => {
    const wrapperRef = useRef<HTMLDivElement>(null);
    const [isVisible, setIsVisible] = useState(true);

    // below is the same as componentDidMount and componentDidUnmount
    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    }, []);


    const handleClickOutside = event => {
       const domNode = ReactDOM.findDOMNode(wrapperRef);
       // error is coming from below
       if (!domNode || !domNode.contains(event.target)) {
          setIsVisible(false);
       }
    }

    return(
       <div ref={wrapperRef}>
         <p>Menu</p>
       </div>
    )
}
peter flanagan
  • 9,195
  • 26
  • 73
  • 127
  • 1
    You should probably read [this](https://stackoverflow.com/questions/50597152/what-is-the-alternative-for-reactdom-finddomnode-as-it-is-deprecated-now), and [this](https://stackoverflow.com/questions/43435881/should-i-use-ref-or-finddomnode-to-get-react-root-dom-node-of-an-element), and [this](https://stackoverflow.com/questions/45652903/reactdom-finddomnode-return-null-with-refs)...and then reconsider your question. – Randy Casburn Jan 27 '19 at 19:17
  • Have a look at my stack answer incl. working example with react hooks and outside click detection here: https://stackoverflow.com/questions/32553158/detect-click-outside-react-component/54292872#54292872. Does that help you? – ford04 Jun 11 '19 at 19:50

4 Answers4

40

the useRef API should be used like this:

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const wrapperRef = useRef(null);
  const [isVisible, setIsVisible] = useState(true);

  // below is the same as componentDidMount and componentDidUnmount
  useEffect(() => {
    document.addEventListener("click", handleClickOutside, false);
    return () => {
      document.removeEventListener("click", handleClickOutside, false);
    };
  }, []);

  const handleClickOutside = event => {
    if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
      setIsVisible(false);
    }
  };

  return (
    isVisible && (
      <div className="menu" ref={wrapperRef}>
        <p>Menu</p>
      </div>
    )
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
thedude
  • 9,388
  • 1
  • 29
  • 30
  • Perhaps this accepted answer is no longer valid? The analog to the `ref={wrapperRef}` provokes a complaint in my code in 2022. The complaint is "Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?" – Tom Stambaugh Sep 30 '22 at 22:30
  • @TomStambaugh the `ref` here is used to attach to an actual DOM node. If you want your custom component to be able to receive a ref it should be wrapped with `React.forwardRef` – thedude Oct 20 '22 at 10:41
5

I have created this common hook, which can be used for all divs which want this functionality.

import { useEffect } from 'react';

/**
 *
 * @param {*} ref - Ref of your parent div
 * @param {*} callback - Callback which can be used to change your maintained state in your component
 * @author Pranav Shinde 30-Nov-2021
 */
const useOutsideClick = (ref, callback) => {
    useEffect(() => {
        const handleClickOutside = (evt) => {
            if (ref.current && !ref.current.contains(evt.target)) {
                callback(); //Do what you want to handle in the callback
            }
        };
        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    });
};

export default useOutsideClick;

Usage -

  1. Import the hook in your component
  2. Add a ref to your wrapper div and pass it to the hook
  3. add a callback function to change your state(Hide the dropdown/modal)
import React, { useRef } from 'react';
import useOutsideClick from '../../../../hooks/useOutsideClick';

const ImpactDropDown = ({ setimpactDropDown }) => {
    const impactRef = useRef();

    useOutsideClick(impactRef, () => setimpactDropDown(false)); //Change my dropdown state to close when clicked outside

    return (
        <div ref={impactRef} className="wrapper">
            {/* Your Dropdown or Modal */}
        </div>
    );
};

export default ImpactDropDown;

pranav shinde
  • 1,260
  • 13
  • 11
4

Check out this library from Andarist called use-onclickoutside.

import * as React from 'react'
import useOnClickOutside from 'use-onclickoutside'

export default function Modal({ close }) {
  const ref = React.useRef(null)
  useOnClickOutside(ref, close)

  return <div ref={ref}>{'Modal content'}</div>
}
Paul Razvan Berg
  • 16,949
  • 9
  • 76
  • 114
0

An alternative solution is to use a full-screen invisible box.

import React, { useState } from 'react';

const Menu = () => {

    const [active, setActive] = useState(false);

    return(

        <div>
            {/* The menu has z-index = 1, so it's always on top */}
            <div className = 'Menu' onClick = {() => setActive(true)}
                {active
                ? <p> Menu active   </p>
                : <p> Menu inactive </p>
                }
            </div>
            {/* This is a full-screen box with z-index = 0 */}
            {active
            ? <div className = 'Invisible' onClick = {() => setActive(false)}></div>
            : null
            }
        </div>

    );

}

And the CSS:

.Menu{
    z-index: 1;
}
.Invisible{
    height: 100vh;
    left: 0;
    position: fixed;
    top: 0;
    width: 100vw;
    z-index: 0;
}
Erik Martín Jordán
  • 4,332
  • 3
  • 26
  • 36