16

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>
    )
}
2
  • 1
    You should probably read this, and this, and this...and then reconsider your question. Commented Jan 27, 2019 at 19:17
  • Have a look at my stack answer incl. working example with react hooks and outside click detection here: stackoverflow.com/questions/32553158/…. Does that help you? Commented Jun 11, 2019 at 19:50

4 Answers 4

42

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);
Sign up to request clarification or add additional context in comments.

2 Comments

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()?"
@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
9

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;

Comments

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>
}

Comments

1

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;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.