React 컴포넌트 외부의 클릭 감지


410

이 기사 에서 설명하는 것처럼 클릭 이벤트가 구성 요소 외부에서 발생했는지 감지하는 방법을 찾고 있습니다. jQuery closest ()는 click 이벤트의 대상에 부모 요소 중 하나 인 dom 요소가 있는지 확인하는 데 사용됩니다. 일치하는 경우 click 이벤트는 하위 중 하나에 속하므로 구성 요소 외부에있는 것으로 간주되지 않습니다.

그래서 내 구성 요소에서 클릭 핸들러를 창에 연결하고 싶습니다. 처리기가 시작되면 대상을 내 구성 요소의 dom 자식과 비교해야합니다.

click 이벤트에는 이벤트가 이동 한 dom 경로를 보유하는 것으로 보이는 "path"와 같은 속성이 포함되어 있습니다. 나는 무엇을 비교할 것인지 또는 어떻게 그것을 가장 잘 횡단해야하는지 잘 모르겠습니다.


클릭 핸들러를 창이 아닌 부모에 연결할 수 있습니까?
— J. Mark Stevens

클릭 처리기를 부모에 연결하면 해당 요소 또는 해당 자식 중 하나를 클릭했을 때 알고 있지만 클릭 한 다른 모든 위치 를 감지해야 하므로 처리기를 창에 연결해야합니다.
— Thijs Koerselman

이전 응답 후 기사를 보았습니다. 상단 구성 요소에서 clickState를 설정하고 아이들의 클릭 동작을 전달하는 방법은 무엇입니까? 그런 다음 아이들의 소품을 확인하여 열린 상태를 관리합니다.
— J. Mark Stevens

최상위 구성 요소는 내 앱입니다. 그러나 청취 구성 요소는 몇 단계 깊고 돔에서 엄격한 위치를 갖지 않습니다. 클릭 처리기를 내 앱의 모든 구성 요소에 추가 할 수는 없습니다. 그중 하나가 외부의 다른 곳을 클릭했는지 알고 싶어하기 때문입니다. 다른 구성 요소는이 논리를 인식하지 않아야합니다.이 논리는 끔찍한 종속성과 상용구 코드를 생성하기 때문입니다.
— Thijs Koerselman

5
나는 당신에게 아주 좋은 라이브러리를 추천하고 싶습니다. AirBnb에 의해 만들어진 : github.com/airbnb/react-outside-click-handler
— Osoian Marcel

답변:


683

React 16.3+의 Refs 사용법이 변경되었습니다.

다음 솔루션은 ES6을 사용하며 방법을 통해 참조 설정뿐만 아니라 바인딩에 대한 모범 사례를 따릅니다.

그것을 실제로 보려면 :

클래스 구현 :

import React, { Component } from 'react';

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
    constructor(props) {
        super(props);

        this.wrapperRef = React.createRef();
        this.setWrapperRef = this.setWrapperRef.bind(this);
        this.handleClickOutside = this.handleClickOutside.bind(this);
    }

    componentDidMount() {
        document.addEventListener('mousedown', this.handleClickOutside);
    }

    componentWillUnmount() {
        document.removeEventListener('mousedown', this.handleClickOutside);
    }

    /**
     * Alert if clicked on outside of element
     */
    handleClickOutside(event) {
        if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
            alert('You clicked outside of me!');
        }
    }

    render() {
        return <div ref={this.wrapperRef}>{this.props.children}</div>;
    }
}

OutsideAlerter.propTypes = {
    children: PropTypes.element.isRequired,
};

후크 구현 :

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
    useEffect(() => {
        /**
         * Alert if clicked on outside of element
         */
        function handleClickOutside(event) {
            if (ref.current && !ref.current.contains(event.target)) {
                alert("You clicked outside of me!");
            }
        }

        // Bind the event listener
        document.addEventListener("mousedown", handleClickOutside);
        return () => {
            // Unbind the event listener on clean up
            document.removeEventListener("mousedown", handleClickOutside);
        };
    }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function OutsideAlerter(props) {
    const wrapperRef = useRef(null);
    useOutsideAlerter(wrapperRef);

    return <div ref={wrapperRef}>{props.children}</div>;
}

3
그래도 어떻게 테스트합니까? handleClickOutside 함수에 유효한 event.target을 어떻게 보내나요?
— csilk

5
document.addEventListener여기 대신 React의 합성 이벤트를 어떻게 사용할 수 있습니까?
— Ralph David Abernathy

9
@ polkovnikov.ph 1. context생성자가 사용될 경우 생성자의 인수로만 필요합니다. 이 경우에는 사용되지 않습니다. 반응 팀 props이 생성자에서 인수로 사용하도록 권장하는 이유는 this.props호출 전의 사용 super(props)이 정의되지 않아 오류가 발생할 수 있기 때문 입니다. context내부 노드에서 계속 사용할 수 있습니다 context. 이런 식으로 소품을 사용하는 것처럼 구성 요소에서 구성 요소로 전달할 필요가 없습니다. 2. 이것은 단지 문체 적 선호 일 뿐이며,이 경우 논쟁을 보증하지 않습니다.
— 벤 버드

7
다음과 같은 오류가 발생합니다. "this.wrapperRef.contains는 함수가 아닙니다"
— Bogdan

5
@Bogdan, 스타일 구성 요소를 사용할 때 동일한 오류가 발생했습니다. 최상위 레벨에서 <div> 사용을 고려하십시오
— user3040068

149

컨테이너에 이벤트를 첨부하지 않고 나에게 가장 적합한 솔루션은 다음과 같습니다.

특정 HTML 요소는 입력 요소 와 같이 " 초점 " 이라고하는 것을 가질 수 있습니다 . 이러한 요소는 초점을 잃을 때 흐림 이벤트에 반응합니다 .

요소에 초점을 둘 수있는 용량을 제공하려면 tabindex 속성이 -1 이외의 값으로 설정되어 있는지 확인하십시오. 일반 HTML에서는 tabindex속성 을 설정 하지만 React에서는 tabIndex(대문자 사용 I) 을 사용해야 합니다.

당신은 또한 자바 스크립트를 통해 그것을 할 수 있습니다 element.setAttribute('tabindex',0)

이것은 사용자 정의 드롭 다운 메뉴를 만들기 위해 내가 사용했던 것입니다.

var DropDownMenu = React.createClass({
    getInitialState: function(){
        return {
            expanded: false
        }
    },
    expand: function(){
        this.setState({expanded: true});
    },
    collapse: function(){
        this.setState({expanded: false});
    },
    render: function(){
        if(this.state.expanded){
            var dropdown = ...; //the dropdown content
        } else {
            var dropdown = undefined;
        }

        return (
            <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                <div className="currentValue" onClick={this.expand}>
                    {this.props.displayValue}
                </div>
                {dropdown}
            </div>
        );
    }
});

10
접근성이 문제가되지 않습니까? 초점이 들어오고 나가는 시점을 감지하기 위해 추가 요소에 초점을 맞출 수 있기 때문에 상호 작용이 드롭 다운 값에 있어야합니까?
— Thijs Koerselman

2
이것은 매우 흥미로운 접근법입니다. 내 상황에 완벽하게 작동했지만 이것이 접근성에 어떤 영향을 미치는지 배우고 싶습니다.
— klinore

17
나는이 "작동", 그것은 멋진 작은 해킹이 있습니다. 내 문제는 드롭 다운 콘텐츠가 display:absolute드롭 다운이 부모 div의 크기에 영향을 미치지 않도록하는 것입니다. 이것은 드롭 다운에서 항목을 클릭하면 onblur가 발생한다는 것을 의미합니다.
— Dave

2
이 접근법에 문제가 있었으므로 드롭 다운 내용의 모든 요소가 onClick과 같은 이벤트를 발생시키지 않습니다.
— AnimaSola

8
드롭 다운 컨텐츠에 예를 들어 양식 입력과 같은 포커스 가능한 요소가 포함 된 경우 다른 문제가 발생할 수 있습니다. 그들은 당신의 초점을 훔치고 onBlur 드롭 다운 컨테이너에서 트리거 expanded 되며 false로 설정됩니다.
— Kamagatos

106

나는 같은 문제에 갇혀 있었다. 나는 파티에 조금 늦었지만 나에게는 이것이 정말 좋은 해결책이다. 잘만되면 그것은 다른 누군가에게 도움이 될 것입니다. 에서 가져와야합니다 findDOMNode.react-dom

import ReactDOM from 'react-dom';
// ... ✂

componentDidMount() {
    document.addEventListener('click', this.handleClickOutside, true);
}

componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
}

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

반응 고리 접근 (16.8 +)

라는 재사용 가능한 후크를 만들 수 있습니다 useComponentVisible.

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

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

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    });

    return { ref, isComponentVisible, setIsComponentVisible };
}

그런 다음 구성 요소에서 기능을 추가하여 다음을 수행하십시오.

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );

}

codesandbox 예제를 찾으 십시오.


1
@LeeHanKyeol 전체적으로 아님-이 답변은 이벤트 처리의 캡처 단계에서 이벤트 핸들러를 호출하는 반면, 연결된 답변은 버블 단계 동안 이벤트 핸들러를 호출합니다.
— stevejay

3
이것이 정답입니다. 절대 위치 지정이있는 드롭 다운 메뉴에서 완벽하게 작동했습니다.
— dishwasherWithProgrammingSkill

1
ReactDOM.findDOMNode더 이상 사용되지 않습니다. ref콜백을 사용해야합니다 : github.com/yannickcr/eslint-plugin-react/issues/…
— dain

2
훌륭하고 깨끗한 솔루션
— Karim

1
200ms 후에 다시 'true'로 다시 설정하기 위해 구성 요소를 해킹해야했지만 그렇지 않으면 메뉴가 다시 표시되지 않습니다. 감사합니다!
— Lee Moe

87

여기에서 많은 방법을 시도한 후 github.com/Pomax/react-onclickoutside 를 사용하기로 결정했습니다 .

npm을 통해 모듈을 설치하고 내 구성 요소로 가져 왔습니다.

import onClickOutside from 'react-onclickoutside'

그런 다음 컴포넌트 클래스에서 handleClickOutside메소드를 정의했습니다 .

handleClickOutside = () => {
  console.log('onClickOutside() method called')
}

그리고 내 구성 요소를 내보낼 때 다음과 같이 포장했습니다 onClickOutside().

export default onClickOutside(NameOfComponent)

그게 다야.


2
Pablo가 제안한 tabIndex/ onBlur접근법에 비해 이것에 대한 구체적인 장점이 있습니까? 구현은 어떻게 작동하며 작동 onBlur방식 과 접근 방식은 어떻게 다릅니 까?
— Mark Amery

4
tabIndex 핵을 사용하여 전용 구성 요소를 사용하는 것이 좋습니다. 찬성!
— Atul Yadav

5
@MarkAmery- tabIndex/onBlur접근 방식에 의견을 달았습니다 . position:absolute메뉴가 다른 콘텐츠 위로 마우스를 가져가는 등 드롭 다운이 일 때는 작동하지 않습니다 .
— Beau Smith

4
tabindex보다 이것을 사용하는 또 다른 장점은 자식 요소에 초점을 맞추면 tabindex 솔루션이 블러 이벤트를 발생
— 시킨다는 것입니다.

1
@BeauSmith-정말 고마워요! 내 하루를 구했다!
— Mika

38

나는 벤 알퍼트에 대한 해결책 덕분에 발견 discuss.reactjs.org . 제안 된 접근법은 문서에 처리기를 연결하지만 문제가되는 것으로 나타났습니다. 내 트리에서 구성 요소 중 하나를 클릭하면 다시 렌더링이 발생하여 업데이트시 클릭 한 요소가 제거되었습니다. 문서 본문 핸들러가 호출 되기 전에 React의 재 렌더가 발생 하므로 요소가 트리의 "내부"로 감지되지 않았습니다.

이에 대한 해결책은 응용 프로그램 루트 요소에 처리기를 추가하는 것이 었습니다.

본관:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

구성 요소:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

export default class ClickListener extends Component {

  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  }

  componentDidMount () {
    window.__myapp_container.addEventListener('click', this.handleDocumentClick)
  }

  componentWillUnmount () {
    window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
  }

  /* using fat arrow to bind to instance */
  handleDocumentClick = (evt) => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt)
    }
  }

  render () {
    return (
      <div ref='area'>
       {this.props.children}
      </div>
    )
  }
}

8
이것은 React 0.14.7에서 더 이상 작동하지 않습니다. 아마도 React가 무언가를 변경했거나 React의 모든 변경 사항에 코드를 적용 할 때 오류가 발생했을 수 있습니다. 대신 매력처럼 작동 하는 github.com/Pomax/react-onclickoutside 를 사용 하고 있습니다.
— Nicole

1
흠. 왜 이것이 효과가 있는지 알 수 없습니다. 앱의 루트 DOM 노드에있는 핸들러가 다른 핸들러에 의해 다시 렌더링되기 전에 실행되어야하는 이유는 무엇 document입니까?
— Mark Amery

1
순수한 UX 포인트 : mousedown아마도 click여기 보다 더 나은 핸들러 일 것 입니다. 대부분의 응용 프로그램에서 메뉴를 클릭하여 외부로 닫기 동작은 마우스를 놓을 때가 아니라 마우스를 놓는 순간에 발생합니다. 예를 들어 Stack Overflow의 플래그 또는 공유 대화 상자 나 브라우저의 최상위 메뉴 표시 줄에있는 드롭 다운 중 하나를 사용하여 사용해보십시오 .
— Mark Amery

ReactDOM.findDOMNode문자열 ref은 더 이상 사용되지 않으며 ref콜백을 사용해야합니다 . github.com/yannickcr/eslint-plugin-react/issues/…
— dain

이것은 가장 간단한 해결책이며 document
— 23:47

37

JSConf Hawaii 2020에서 Tanner Linsley의 탁월한 강연을 기반으로 한 후크 구현 :

useOuterClick 후크 API

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

이행

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function handleClick(e) {
      if (
        innerRef.current && 
        callbackRef.current &&
        !innerRef.current.contains(e.target)
      ) {
        callbackRef.current(e);
      }
    }
  }, []); // no need for callback + innerRef dep
  
  return innerRef; // return ref; client can omit `useRef`
}

/*
  Usage 
*/
const Client = () => {
  const [counter, setCounter] = useState(0);
  const innerRef = useOuterClick(e => {
    // counter state is up-to-date, when handler is called
    alert(`Clicked outside! Increment counter to ${counter + 1}`);
    setCounter(c => c + 1);
  });
  return (
    <div>
      <p>Click outside!</p>
      <div id="container" ref={innerRef}>
        Inside, counter: {counter}
      </div>
    </div>
  );
};

ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

useOuterClick에 대한 린 API를 생성하기 위해 가변 참조 를 사용합니다 Client. 를 통해 메모하지 않아도 콜백을 설정할 수 있습니다 useCallback. 콜백 본문은 여전히 ​​최신 소품과 상태에 액세스 할 수 있습니다 (부실 폐쇄 값 없음).


iOS 사용자를위한 참고 사항

iOS는 특정 요소 만 클릭 가능한 것으로 취급합니다. 이 동작을 피하려면을 document포함하여 위쪽 이외의 다른 외부 클릭 리스너를 선택하십시오 body. 예를 div들어 위 예제에서 React 루트 에 리스너를 추가 하고 높이 ( height: 100vh또는 유사한)를 확장하여 모든 외부 클릭을 잡을 수 있습니다. 출처 : quirksmode.org 및 이 답변


너무 불행한 모바일에서는 작동하지 않습니다
— Omar

@Omar는 방금 안드로이드 장치에서 Firefox 67.0.3을 사용하여 내 게시물의 코드 및 상자를 테스트했습니다. 사용하는 브라우저, 오류 등을 자세히 설명해 주시겠습니까?
— ford04

오류는 없지만 크롬, 아이폰 7 이상에서는 작동하지 않습니다. 외부 탭을 감지하지 못합니다. 모바일의 Chrome 개발 도구에서는 작동하지만 실제 iOS 장치에서는 작동하지 않습니다.
— Omar

1
@ ford04 파고 들어 주셔서 감사합니다 . 다음 트릭을 제안하는 다른 스레드 를 우연히 발견했습니다 . @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }내 글로벌 스타일로 이것을 추가하면 문제가 해결 된 것 같습니다.
— 코스타스 사란 토풀 로스

1
@ ford04 예, 이 스레드 에 따르면 btw 는 더 많은 @supports (-webkit-overflow-scrolling: touch)것을 알지 못하더라도 IOS만을 대상으로하는 더 구체적인 미디어 쿼리 가 있습니다! 방금 테스트했으며 작동합니다.
— 코스타스 사란 토풀 로스

30

여기에 다른 답변들 중 어느 것도 나를 위해 일하지 않았습니다. 블러시 팝업을 숨기려고했지만 내용이 절대적으로 배치되었으므로 내부 내용을 클릭해도 onBlur가 실행되었습니다.

나를 위해 일한 접근법이 있습니다.

// Inside the component:
onBlur(event) {
    // currentTarget refers to this component.
    // relatedTarget refers to the element where the user clicked (or focused) which
    // triggered this event.
    // So in effect, this condition checks if the user clicked outside the component.
    if (!event.currentTarget.contains(event.relatedTarget)) {
        // do your thing.
    }
},

도움이 되었기를 바랍니다.


아주 좋아요! 감사. 그러나 제 경우에는 div의 절대 위치로 "현재 위치"를 잡는 것이 문제가되었으므로 마우스를 div를 떠날 때 모든 div를 비활성화하기 위해 입력 및 드롭 다운 캘린더가있는 div에 "OnMouseLeave"를 사용합니다.
— Alex

1
감사! 많이 도와주세요.
— ngenge Lucas

1
나는 첨부 된 방법보다 낫습니다 document. 감사.
— kyw

이 작업을 수행하려면 클릭 한 요소 (relatedTarget)가 초점을 맞출 수 있는지 확인해야합니다. 참조 stackoverflow.com/questions/42764494/...
— xabitrigo

21

[업데이트] 후크를 사용한 React ^ 16.8 솔루션

코드 샌드 박스

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

const SampleComponent = () => {
    const [clickedOutside, setClickedOutside] = useState(false);
    const myRef = useRef();

    const handleClickOutside = e => {
        if (!myRef.current.contains(e.target)) {
            setClickedOutside(true);
        }
    };

    const handleClickInside = () => setClickedOutside(false);

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    });

    return (
        <button ref={myRef} onClick={handleClickInside}>
            {clickedOutside ? 'Bye!' : 'Hello!'}
        </button>
    );
};

export default SampleComponent;

반응이있는 솔루션 ^ 16.3 :

코드 샌드 박스

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;

3
이것이 지금가는 해결책입니다. 오류가 발생 .contains is not a function하면 ref 소품을<div>
— willlma

ref소품을 커스텀 컴포넌트 에 전달하려는 사람들 은React.forwardRef
— onoya를

4

내 접근 방식은 다음과 같습니다 (demo- https : //jsfiddle.net/agymay93/4/ ).

나는 특별한 구성 요소를 만들었고 다음 WatchClickOutside과 같이 사용할 수 있습니다 ( JSX구문을 가정 합니다)

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

WatchClickOutside컴포넌트 코드는 다음과 같습니다 .

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </div>
    );
  }
}

훌륭한 솔루션-사용하기 위해 내용이 짧은 페이지의 경우 본문 대신 문서를 청취하도록 변경했으며 문자열에서 돔 참조로 참조를 변경했습니다. jsfiddle.net/agymay93/9
— Billy Moon

대신 사업부의 사용 범위는 변화의 레이아웃 및 신체의 외부 클릭을 처리 추가 데모 덜 가능성이 높습니다 같이 jsfiddle.net/agymay93/10
— 빌리 달

문자열 ref은 더 이상 사용되지 않으며 ref콜백을 사용해야합니다 . reactjs.org/docs/refs-and-the-dom.html
— dain '

4

이미 많은 답변이 있지만 e.stopPropagation()닫지 않으려는 요소 외부의 반응 링크를 클릭 하지 못하게하고 해결하지 못합니다 .

React에는 자체 인공 이벤트 핸들러가 있기 때문에 문서를 이벤트 리스너의 기반으로 사용할 수 없습니다. e.stopPropagation()React가 문서 자체를 사용하므로이 작업을 수행 해야 합니다. 예를 들어 document.querySelector('body')대신 사용하는 경우 . 반응 링크에서 클릭을 방지 할 수 있습니다. 다음은 클릭 외부를 구현하고 닫는 방법의 예입니다.
이것은 ES6 과 React 16.3을 사용 합니다.

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
    };

    this.insideContainer = React.createRef();
  }

  componentWillMount() {
    document.querySelector('body').addEventListener("click", this.handleClick, false);
  }

  componentWillUnmount() {
    document.querySelector('body').removeEventListener("click", this.handleClick, false);
  }

  handleClick(e) {
    /* Check that we've clicked outside of the container and that it is open */
    if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
      e.preventDefault();
      e.stopPropagation();
      this.setState({
        isOpen: false,
      })
    }
  };

  togggleOpenHandler(e) {
    e.preventDefault();

    this.setState({
      isOpen: !this.state.isOpen,
    })
  }

  render(){
    return(
      <div>
        <span ref={this.insideContainer}>
          <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
        </span>
        <a href="/" onClick({/* clickHandler */})>
          Will not trigger a click when inside is open.
        </a>
      </div>
    );
  }
}

export default App;

3

절대 위치 지정이 필요한 사람들을 위해, 내가 선택한 간단한 옵션은 전체 페이지를 투명한 배경으로 덮는 스타일의 래퍼 구성 요소를 추가하는 것입니다. 그런 다음이 요소에 onClick을 추가하여 내부 구성 요소를 닫을 수 있습니다.

<div style={{
        position: 'fixed',
        top: '0', right: '0', bottom: '0', left: '0',
        zIndex: '1000',
      }} onClick={() => handleOutsideClick()} >
    <Content style={{position: 'absolute'}}/>
</div>

콘텐츠에 클릭 핸들러를 추가하면 바로 이벤트가 상단 div로 전파되어 handlerOutsideClick이 트리거됩니다. 원하는 동작이 아닌 경우 처리기에서 이벤트 생성을 중지하면됩니다.

<Content style={{position: 'absolute'}} onClick={e => {
                                          e.stopPropagation();
                                          desiredFunctionCall();
                                        }}/>

`


이 방법의 문제는 콘텐츠를 클릭 할 수 없다는 것입니다 .div는 대신 클릭을받습니다.
— rbonick

내용이 div에 있으므로 이벤트 전파를 중지하지 않으면 둘 다 클릭을 수신합니다. 이것은 종종 바람직한 동작이지만 클릭이 div로 전파되는 것을 원하지 않으면 콘텐츠 onClick 핸들러에서 eventPropagation을 중지하십시오. 방법을 보여주기 위해 답변을 업데이트했습니다.
— Anthony Garant 2016 년

3

스타일이 지정된 구성 요소를 사용하는 경우 Ben Bud 에서 허용 된 답변을 확장하기 위해 해당 방식으로 참조를 전달하면 "this.wrapperRef.contains is a function"과 같은 오류가 발생합니다.

주석에서 스타일이 지정된 구성 요소를 div로 감싸고 참조를 전달하는 제안 된 수정 프로그램이 작동합니다. 자신에 그런 말로 미루어 문서 는 이미 그 이유와 스타일-구성 요소에서 심판의 적절한 사용을 설명합니다 :

스타일 지정된 구성 요소에 참조 소품을 전달하면 기본 DOM 노드가 아닌 StyledComponent 래퍼의 인스턴스가 제공됩니다. 심판이 작동하는 방식 때문입니다. 랩퍼에서 포커스와 같은 DOM 메소드를 직접 호출 할 수 없습니다. 랩핑 된 실제 DOM 노드에 대한 참조를 얻으려면 대신 콜백을 innerRef prop에 전달하십시오.

이렇게 :

<StyledDiv innerRef={el => { this.el = el }} />

그런 다음 "handleClickOutside"함수 내에서 직접 액세스 할 수 있습니다.

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

이것은 "onBlur"접근 방식에도 적용됩니다.

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}


2

다른 모든 답변에 대한 가장 큰 관심사는 루트 / 부모 다운에서 클릭 이벤트를 필터링해야한다는 것입니다. 가장 쉬운 방법은 위치 : 고정, 드롭 다운 뒤에 z- 인덱스 1로 형제 요소를 설정하고 동일한 구성 요소 내부의 고정 요소에서 클릭 이벤트를 처리하는 것입니다. 주어진 구성 요소에 모든 것을 중앙 집중식으로 유지합니다.

예제 코드

#HTML
<div className="parent">
  <div className={`dropdown ${this.state.open ? open : ''}`}>
    ...content
  </div>
  <div className="outer-handler" onClick={() => this.setState({open: false})}>
  </div>
</div>

#SASS
.dropdown {
  display: none;
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: 100;
  &.open {
    display: block;
  }
}
.outer-handler {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    z-index: 99;
    display: none;
    &.open {
      display: block;
    }
}

1
흠. 여러 인스턴스에서 작동합니까? 아니면 각 고정 요소가 다른 요소의 클릭을 잠재적으로 차단합니까?
— Thijs Koerselman

2
componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

이벤트 path[0]는 마지막으로 클릭 한 항목입니다


3
메모리 관리 문제 등을 방지하기 위해 구성 요소를 마운트 해제 할 때 이벤트 리스너를 제거하십시오.
— agm1984

2

이 모듈을 사용 했습니다 (저자와 관련이 없습니다)

npm install react-onclickout --save

const ClickOutHandler = require('react-onclickout');
 
class ExampleComponent extends React.Component {
 
  onClickOut(e) {
    if (hasClass(e.target, 'ignore-me')) return;
    alert('user clicked outside of the component!');
  }
 
  render() {
    return (
      <ClickOutHandler onClickOut={this.onClickOut}>
        <div>Click outside of me!</div>
      </ClickOutHandler>
    );
  }
}

작업을 훌륭하게 수행했습니다.


환상적으로 작동합니다! 여기에있는 다른 많은 답변보다 훨씬 간단하며 여기에있는 3 가지 다른 솔루션과 달리이 모든 것을받은 "Support for the experimental syntax 'classProperties' isn't currently enabled"즉시 상자에서 바로 작동했습니다. 이 솔루션을 시도하는 사람은 다른 솔루션을 시도 할 때 복사 한 남아있는 코드를 지우십시오. 예를 들어 이벤트 리스너를 추가하면 이와 충돌하는 것 같습니다.
— Frikster

2

나는 이것을 수행하고 반응 ^ 16.3이 필요한 심판 처리에 대한 React 공식 문서를 따라 부분적으로 수행 했습니다 . 이것은 다른 제안을 시도한 후에 나를 위해 일한 유일한 것입니다 ...

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}

1

전략의 예

구성 요소 주위에 래퍼를 만들어 동일한 작업을 수행하는 데 제공되는 솔루션이 마음에 듭니다.

이것이 더 많은 행동이기 때문에 전략을 생각하고 다음을 생각해 냈습니다.

React를 처음 사용하고 유스 케이스에서 보일러 플레이트를 절약하려면 약간의 도움이 필요합니다

당신의 생각을 검토하고 말 해주세요.

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

샘플 사용법

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

노트

전달 된 component수명주기 후크 를 저장하고 이와 비슷한 방법으로 재정의하려고 생각 했습니다.

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

component의 생성자에 전달 된 구성 요소입니다 ClickOutsideBehavior.
이것은이 동작의 사용자로부터 enable / disable 상용구를 제거하지만 아주 좋아 보이지는 않습니다.


1

DROPDOWN 사례에서 Ben Bud의 솔루션 은 잘 작동했지만 onClick 핸들러가있는 별도의 토글 버튼이있었습니다. 따라서 외부 클릭 로직이 onClick 전환기 버튼과 충돌했습니다. 다음은 버튼의 참조를 전달하여 해결하는 방법입니다.

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

/**
 * Hook that triggers onClose when clicked outside of ref and buttonRef elements
 */
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
  useEffect(() => {

    function handleClickOutside(event) {
      /* clicked on the element itself */
      if (ref.current && !ref.current.contains(event.target)) {
        return;
      }

      /* clicked on the toggle button */
      if (buttonRef.current && !buttonRef.current.contains(event.target)) {
        return;
      }

      /* If it's something else, trigger onClose */
      onOutsideClick();
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function DropdownMenu(props) {
  const wrapperRef = useRef(null);
  const buttonRef = useRef(null);
  const [dropdownVisible, setDropdownVisible] = useState(false);

  useOutsideClicker(wrapperRef, buttonRef, closeDropdown);

  const toggleDropdown = () => setDropdownVisible(visible => !visible);

  const closeDropdown = () => setDropdownVisible(false);

  return (
    <div>
      <button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
      {dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
    </div>
  );
}

0

당신이 작은 구성 요소를 사용하려면 (466 바이트는 gzip으로 압축) 이미이 기능을 존재 그 다음이 라이브러리를 확인하실 수 있습니다 반응-outclick을 .

라이브러리의 장점은 구성 요소 외부와 다른 내부의 클릭을 감지 할 수 있다는 것입니다. 다른 유형의 이벤트 감지도 지원합니다.


0

이 기능에 이미 존재하는 작은 컴포넌트 (466 바이트 gzipped)를 사용하려면이 라이브러리 react-outclick을 확인하십시오 . 반응 구성 요소 또는 jsx 요소 외부의 이벤트를 캡처합니다.

라이브러리의 장점은 구성 요소 외부와 다른 내부의 클릭을 감지 할 수 있다는 것입니다. 다른 유형의 이벤트 감지도 지원합니다.


0

나는 이것이 오래된 질문이라는 것을 알고 있지만, 나는 이것을 계속 겪고 있으며 간단한 형식으로 이것을 알아내는 데 많은 어려움을 겪었다. 따라서 이것이 누군가의 삶을 조금 더 쉽게 만들려면 airbnb의 OutsideClickHandler를 사용하십시오. 자신의 코드를 작성하지 않고이 작업을 수행하는 가장 간단한 플러그인입니다.

예:

hideresults(){
   this.setState({show:false})
}
render(){
 return(
 <div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() => 
  {this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
 )
}

0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

0

당신은 당신이 당신의 문제를 해결하는 간단한 방법이 될 수 있습니까, 나는 당신에게 보여줍니다 :

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

const openCloseDropDown = () =>{
 setDropDownStatus(prev => !prev)
}

const closeDropDown = ()=> {
 if(dropDwonStatus){
   setDropDownStatus(false)
 }
}
.
.
.
<parent onClick={closeDropDown}>
 <child onClick={openCloseDropDown} />
</parent>

이것은 나를 위해 작동합니다, 행운을 빕니다;)


-1

아래 기사에서 이것을 찾았습니다.

render () {return ({this.node = node;}}> 팝 오버 토글 {this.state.popupVisible && (나는 팝 오버입니다!)}); }}

다음은이 문제에 대한 유용한 기사입니다. "React 구성 요소 외부에서 클릭 처리" https://larsgraubner.com/handle-outside-clicks-react/


스택 오버플로에 오신 것을 환영합니다! : 그냥 링크입니다 답변은 일반적으로 눈살을 찌푸리게 meta.stackoverflow.com/questions/251006/... . 앞으로 질문과 관련된 정보가 포함 된 견적 또는 코드 예제를 포함하십시오. 건배!
— Fred Stark

-1

onClick최상위 컨테이너에 핸들러를 추가 하고 사용자가 클릭 할 때마다 상태 값을 증가시킵니다. 해당 값을 관련 구성 요소에 전달하고 값이 변경 될 때마다 작업을 수행 할 수 있습니다.

이 경우 값이 변경 this.closeDropdown()될 때마다 호출 합니다 clickCount.

incrementClickCount내 방법 화재 .app용기가 아니라는 .dropdown우리가 사용하기 때문에 event.stopPropagation()이벤트 버블 링을 방지 할 수 있습니다.

코드가 다음과 같이 보일 수 있습니다.

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            clickCount: 0
        };
    }
    incrementClickCount = () => {
        this.setState({
            clickCount: this.state.clickCount + 1
        });
    }
    render() {
        return (
            <div className="app" onClick={this.incrementClickCount}>
                <Dropdown clickCount={this.state.clickCount}/>
            </div>
        );
    }
}
class Dropdown extends Component {
    constructor(props) {
        super(props);
        this.state = {
            open: false
        };
    }
    componentDidUpdate(prevProps) {
        if (this.props.clickCount !== prevProps.clickCount) {
            this.closeDropdown();
        }
    }
    toggleDropdown = event => {
        event.stopPropagation();
        return (this.state.open) ? this.closeDropdown() : this.openDropdown();
    }
    render() {
        return (
            <div className="dropdown" onClick={this.toggleDropdown}>
                ...
            </div>
        );
    }
}

누가 downvoting인지 확실하지 않지만이 솔루션은 바인딩 / 바인딩 해제 이벤트 리스너에 비해 매우 깨끗합니다. 이 구현과 관련하여 전혀 문제가 없었습니다.
— wdm

-1

이벤트 리스너를 사용하여 드롭 다운에 '초점' 솔루션을 작동 시키려면 onClick 대신 onMouseDown 이벤트를 사용 하여 추가 할 수 있습니다 . 그렇게하면 이벤트가 시작되고 팝업이 다음과 같이 닫힙니다.

<TogglePopupButton
                    onClick = { this.toggleDropup }
                    tabIndex = '0'
                    onBlur = { this.closeDropup }
                />
                { this.state.isOpenedDropup &&
                <ul className = { dropupList }>
                    { this.props.listItems.map((item, i) => (
                        <li
                            key = { i }
                            onMouseDown = { item.eventHandler }
                        >
                            { item.itemName}
                        </li>
                    ))}
                </ul>
                }

-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentWillMount() {
    document.addEventListener('mousedown', this.handleClickOutside, false); 
  }

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}

잊지 마세요import ReactDOM from 'react-dom';
— dipole_moment

-1

나는 모든 경우에 해결책을 만들었다.

상위 구성 요소를 사용하여 외부에서 클릭을 수신하려는 구성 요소를 래핑해야합니다.

이 컴포넌트 예제에는 함수를 수신하는 "onClickedOutside"소품이 하나만 있습니다.

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}

-1

UseOnClickOutside Hook-반응 16.8 +

일반적인 useOnOutsideClick 함수 만들기

export const useOnOutsideClick = handleOutsideClick => {
  const innerBorderRef = useRef();

  const onClick = event => {
    if (
      innerBorderRef.current &&
      !innerBorderRef.current.contains(event.target)
    ) {
      handleOutsideClick();
    }
  };

  useMountEffect(() => {
    document.addEventListener("click", onClick, true);
    return () => {
      document.removeEventListener("click", onClick, true);
    };
  });

  return { innerBorderRef };
};

const useMountEffect = fun => useEffect(fun, []);

그런 다음 기능 부품에 고리를 사용하십시오.

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {

  const [open, setOpen] = useState(false);
  const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));

  return (
    <div>
      <button onClick={() => setOpen(true)}>open</button>
      {open && (
        <div ref={innerBorderRef}>
           <SomeChild/>
        </div>
      )}
    </div>
  );

};

데모 링크

@ pau1fitzgerald의 답변에서 부분적으로 영감을 얻었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.