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')대신 사용하는 경우 . 반응 링크에서 클릭을 방지 할 수 있습니다. 다음은 클릭 외부를 구현하고 닫는 방법의 예입니다.
이것은 ES6React 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.