Detectar clic fuera del componente React

620

Estoy buscando una forma de detectar si ocurrió un evento de clic fuera de un componente, como se describe en este artículo . jQuery más cercano () se usa para ver si el objetivo de un evento de clic tiene el elemento dom como uno de sus padres. Si hay una coincidencia, el evento click pertenece a uno de los hijos y, por lo tanto, no se considera que esté fuera del componente.

Entonces, en mi componente, quiero adjuntar un controlador de clic a la ventana. Cuando se dispara el controlador, necesito comparar el objetivo con los hijos dom de mi componente.

El evento click contiene propiedades como "ruta" que parece contener la ruta dom que ha recorrido el evento. No estoy seguro de qué comparar o cómo atravesarlo mejor, y creo que alguien ya debe haberlo puesto en una función de utilidad inteligente ... ¿No?

7
  • ¿Podría adjuntar el controlador de clic al padre en lugar de a la ventana? 13/09/15 a las 18:47
  • Si adjunta un controlador de clic al padre, sabrá cuándo se hace clic en ese elemento o en uno de sus hijos, pero necesito detectar todos los demás lugares en los que se hace clic, por lo que el controlador debe estar adjunto a la ventana. 13/09/15 a las 18:50
  • Miré el artículo después de la respuesta anterior. ¿Qué tal establecer un clickState en el componente superior y pasar las acciones de clic de los niños? Luego, verificaría los accesorios en los niños para administrar el estado de apertura y cierre. 13/09/15 a las 19:01
  • El componente superior sería mi aplicación. Pero el componente de escucha tiene varios niveles de profundidad y no tiene una posición estricta en el dom. No puedo agregar controladores de clic a todos los componentes de mi aplicación solo porque uno de ellos está interesado en saber si hizo clic en algún lugar fuera de ella. Otros componentes no deberían ser conscientes de esta lógica porque crearía dependencias terribles y código repetitivo. 13/09/15 a las 19:14
  • 10
    Me gustaría recomendarle una biblioteca muy buena. creado por AirBnb: github.com/airbnb/react-outside-click-handler 5 abr 19 a las 13:35
1083

Se modificó el uso de referencias en React 16.3+.

La siguiente solución usa ES6 y sigue las mejores prácticas para vincular y establecer la referencia a través de un método.

Para verlo en acción:

Implementación de clases:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

/**
 * 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,
};

Implementación de Hooks:

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>;
}
55
  • 7
    ¿Cómo puedes usar los eventos sintéticos de React, en lugar del document.addEventListeneraquí? 26/07/17 a las 10:13
  • 11
    @ polkovnikov.ph 1. contextsolo es necesario como argumento en el constructor si se usa. No se está utilizando en este caso. La razón por la que el equipo de reacción recomienda tener propscomo argumento en el constructor es porque el uso de this.propsantes de llamar super(props)no estará definido y puede dar lugar a errores. contexttodavía está disponible en los nodos internos, ese es el propósito de context. De esta manera, no tiene que pasarlo de un componente a otro como lo hace con los accesorios. 2. Esto es solo una preferencia estilística y no justifica un debate en este caso.
    Ben Bud
    8 de septiembre de 2017 a las 3:15
  • 22
    Recibo el siguiente error: "this.wrapperRef.contains no es una función"
    Bogdan
    15 de mayo de 2018 a las 12:01
  • 7
    @Bogdan, recibía el mismo error al usar un componente con estilo. Considere usar un <div> en el nivel superior 26 de mayo de 2018 a las 23:29
  • 5
    Gracias por esto. Funcionó para algunos de mis casos, pero no para una ventana emergente display: absolute, siempre se disparaba handleClickOutsidecuando hacía clic en cualquier lugar, incluido el interior. Terminé cambiando a react-outside-click-handler, que parece cubrir este caso de alguna manera
    xaphod
    24 jul.20 a las 15:22
178

Aquí está la solución que mejor me funcionó sin adjuntar eventos al contenedor:

Ciertos elementos HTML pueden tener lo que se conoce como " foco ", por ejemplo, elementos de entrada. Esos elementos también responderán al evento de desenfoque , cuando pierdan ese enfoque.

Para darle a cualquier elemento la capacidad de tener el foco, solo asegúrese de que su atributo tabindex esté configurado en cualquier cosa que no sea -1. En HTML normal eso sería estableciendo el tabindexatributo, pero en React tienes que usar tabIndex(nota la mayúscula I).

También puedes hacerlo a través de JavaScript con element.setAttribute('tabindex',0)

Esto es para lo que lo estaba usando, para hacer un menú desplegable personalizado.

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>
        );
    }
});
16
  • 13
    ¿No crearía esto problemas de accesibilidad? Dado que hace que un elemento adicional se pueda enfocar solo para detectar cuándo está entrando / saliendo del foco, pero la interacción debe estar en los valores desplegables, ¿no? 5 de agosto de 2016 a las 8:06
  • 2
    Este es un enfoque muy interesante. Funcionó perfectamente para mi situación, pero me interesaría saber cómo esto podría afectar la accesibilidad.
    klinore
    1 de septiembre de 2016 a las 14:25
  • 21
    Tengo este "trabajo" hasta cierto punto, es un pequeño truco genial. Mi problema es que mi contenido desplegable es display:absolutepara que el menú desplegable no afecte el tamaño del div principal. Esto significa que cuando hago clic en un elemento del menú desplegable, se activa el onblur.
    Dave
    7/10/2016 a las 17:37
  • 3
    Tuve problemas con este enfoque, cualquier elemento en el contenido desplegable no activa eventos como onClick. 3 de enero de 2017 a las 5:56
  • 8
    Es posible que se enfrente a otros problemas si el contenido desplegable contiene algunos elementos enfocables, como entradas de formulario, por ejemplo. Robarán su enfoque, onBlur se activarán en su contenedor desplegable y expanded se establecerán en falso. 4 de julio de 2017 a las 4:47
169

Estaba atascado en el mismo problema. Llego un poco tarde a la fiesta aquí, pero para mí es una muy buena solución. Con suerte, será de ayuda para otra persona. Necesitas importar findDOMNodedesdereact-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
        });
    }
}

Enfoque de React Hooks (16.8 +)

Puede crear un gancho reutilizable llamado 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 };
}

Luego, en el componente que desea agregar la funcionalidad para hacer lo siguiente:

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

}

Encuentre un ejemplo de codesandbox aquí.

15
  • 1
    @LeeHanKyeol No del todo: esta respuesta invoca los controladores de eventos durante la fase de captura del manejo de eventos, mientras que la respuesta vinculada a invoca los controladores de eventos durante la fase de burbuja. 3 de diciembre de 2017 a las 10:33
  • 3
    Esta debería ser la respuesta aceptada. Funcionó perfectamente para menús desplegables con posicionamiento absoluto. 13/12/2017 a las 17:53
  • 3
    ReactDOM.findDOMNodey está en desuso, debería usar refdevoluciones de llamada: github.com/yannickcr/eslint-plugin-react/issues/…
    dain
    1 de marzo de 2018 a las 13:09
  • 3
    gran y limpia solución
    Karim
    11 de mayo de 2018 a las 11:55
  • 3
    Esto es genial porque es reutilizable. Perfecto. 18/11/20 a las 11:42
105

Después de probar muchos métodos aquí, decidí usar github.com/Pomax/react-onclickoutside debido a lo completo que es.

Instalé el módulo a través de npm y lo importé a mi componente:

import onClickOutside from 'react-onclickoutside'

Luego, en mi clase de componente definí el handleClickOutsidemétodo:

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

Y al exportar mi componente lo envolví en onClickOutside():

export default onClickOutside(NameOfComponent)

Eso es todo.

8
  • 2
    ¿Tiene esto alguna ventaja concreta sobre el enfoque tabIndex/ onBlurpropuesto por Pablo ? ¿Cómo funciona la implementación y en qué se diferencia su comportamiento del onBlurenfoque? 17/01/17 a las 15:20
  • 5
    Es mejor usar un componente dedicado que usar un truco tabIndex. ¡Votando a favor! 23 de enero de 2017 a las 18:55
  • 7
    @MarkAmery - Puse un comentario sobre el tabIndex/onBlurenfoque. No funciona cuando está el menú desplegable position:absolute, como un menú que se cierne sobre otro contenido. 28 de enero de 2017 a las 0:11
  • 5
    Otra ventaja de usar esto sobre tabindex es que la solución tabindex también dispara un evento de desenfoque si se enfoca en un elemento secundario 19/04/2017 a las 19:24
  • 1
    @BeauSmith - ¡Muchas gracias! ¡Salvó mi día!
    Mika
    22/01/20 a las 16:07
80

Implementación de gancho basada en la excelente charla de Tanner Linsley en JSConf Hawaii 2020 :

useOuterClick API

const Client = () => {
  const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
  return <div ref={innerRef}> Inside </div> 
};

Implementación

function useOuterClick(callback) {
  const callbackRef = useRef(); // initialize mutable ref, which stores callback
  const innerRef = useRef(); // returned to client, who marks "border" element

  // update cb on each render, so second useEffect has access to current value 
  useEffect(() => { callbackRef.current = callback; });
  
  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
    function handleClick(e) {
      if (innerRef.current && callbackRef.current && 
        !innerRef.current.contains(e.target)
      ) callbackRef.current(e);
    }
  }, []); // no dependencies -> stable click listener
      
  return innerRef; // convenience for client (doesn't need to init ref himself) 
}

Aquí hay un ejemplo de trabajo:

/*
  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>

Puntos clave

  • useOuterClickhace uso de referencias mutables para proporcionar una ClientAPI ajustada
  • oyente de clics estable durante la vida útil del componente contenedor ( []deps)
  • Client puede configurar la devolución de llamada sin necesidad de memorizarla useCallback
  • el cuerpo de devolución de llamada tiene acceso a los accesorios y el estado más recientes, sin valores de cierre obsoletos

(Nota al margen para iOS)

iOS en general trata solo ciertos elementos como en los que se puede hacer clic. Para hacer que los clics externos funcionen, elija un oyente de clics diferente a document: nada hacia arriba, incluido body. Por ejemplo, agregue un oyente en la raíz de React divy expanda su altura, como height: 100vh, para capturar todos los clics externos. Fuente: quirksmode.org

7
  • No hay errores, pero no funciona en Chrome, iPhone 7+. No detecta los grifos del exterior. En las herramientas de desarrollo de Chrome en dispositivos móviles, funciona, pero en un dispositivo iOS real no funciona.
    Omar
    21/06/19 a las 22:13
  • @Omar parece ser una rareza de IOS muy específica. Mire aquí: quirksmode.org/blog/archives/2014/02/mouse_event_bub.html , stackoverflow.com/questions/18524177/… , gravitydept.com/blog/js-click-event-bubbling-on-ios . La solución alternativa más simple que puedo imaginar es la siguiente: en el ejemplo de codesandbox, configure un controlador de clic vacío para el div raíz como este:, de <div onClick={() => {}}> ... </div>modo que IOS registre todos los clics desde el exterior. Me funciona en Iphone 6+. ¿Eso resuelve tu problema?
    ford04
    22/06/19 a las 11:44
  • Tendré que probar. pero gracias por la solución. Sin embargo, ¿no parece torpe?
    Omar
    23/06/19 a las 3:35
  • 2
    @ ford04 gracias por averiguarlo, me encontré con otro hilo que sugería el siguiente truco. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Al agregar esto en mis estilos globales, parece que solucionó el problema. 2 jul 2019 a las 15:30
  • 1
    @ ford04 sí, por cierto, de acuerdo con este hilo, hay una consulta de medios aún más específica @supports (-webkit-overflow-scrolling: touch)que se dirige solo a IOS, ¡aunque no sé más cuánto! Lo acabo de probar y funciona. 3/07/19 a las 15:24
42

Ninguna de las otras respuestas aquí funcionó para mí. Estaba tratando de ocultar una ventana emergente sobre el desenfoque, pero dado que el contenido estaba absolutamente posicionado, el onBlur se disparaba incluso con el clic del contenido interno también.

Aquí hay un enfoque que funcionó para mí:

// 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.
    }
},

Espero que esto ayude.

7
  • ¡Muy bien! Gracias. Pero en mi caso fue un problema capturar el "lugar actual" con la posición absoluta para el div, así que uso "OnMouseLeave" para el div con entrada y el calendario desplegable para deshabilitar todos los div cuando el mouse deja el div.
    Alex
    12/10/2017 a las 8:22
  • 2
    ¡Gracias! Ayúdame mucho. 29/08/18 a las 17:31
  • 2
    Me gusta esto más que los métodos que se adjuntan document. Gracias.
    kyw
    21 mar 2019 a las 6:02
  • 2
    Para que esto funcione, debe asegurarse de que se pueda enfocar el elemento en el que se hizo clic (relatedTarget). Consulte stackoverflow.com/questions/42764494/… 13 de mayo de 2020 a las 8:32
  • 3
    Honestamente, debería ser la respuesta aceptada, la solución más limpia ITT
    whirish
    10 de enero a las 2:41
42

[Actualización] Solución con React ^ 16.8 usando Hooks

CódigoSandbox

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;

Solución con React ^ 16.3 :

CódigoSandbox

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;
2
  • 3
    Esta es la solución a la que recurrir ahora. Si recibe el error .contains is not a function, puede deberse a que está pasando la referencia de referencia a un componente personalizado en lugar de un elemento DOM real como un<div>
    willlma
    14/11/18 a las 0:24
  • Para aquellos que intentan pasar refprop a un componente personalizado, es posible que desee echar un vistazo aReact.forwardRef
    onoya
    18/07/19 a las 5:44
40

Encontré una solución gracias a Ben Alpert en discus.reactjs.org . El enfoque sugerido adjunta un controlador al documento, pero resultó ser problemático. Al hacer clic en uno de los componentes de mi árbol, se produjo una repetición que eliminó el elemento en el que se hizo clic en la actualización. Debido a que la reproducción de React ocurre antes de que se llame al controlador del cuerpo del documento, el elemento no se detectó como "dentro" del árbol.

La solución a esto fue agregar el controlador en el elemento raíz de la aplicación.

principal:

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

componente:

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>
    )
  }
}
5
  • 8
    Esto ya no me funciona con React 0.14.7; tal vez React cambió algo, o tal vez cometí un error al adaptar el código a todos los cambios en React. En cambio, estoy usando github.com/Pomax/react-onclickoutside, que funciona a la perfección .
    Nicole
    5/04/2016 a las 11:40
  • 1
    Mmm. No veo ninguna razón por la que esto debería funcionar. ¿Por qué debería garantizarse que un controlador en el nodo DOM raíz de la aplicación se activará antes de que otro controlador active una repetición si uno documentno lo está? 2 de abril de 2017 a las 8:38
  • 2
    Un punto de UX puro: mousedownprobablemente sería un mejor controlador que clickaquí. En la mayoría de las aplicaciones, el comportamiento de cerrar el menú haciendo clic en el exterior ocurre en el momento en que presiona el mouse, no cuando lo suelta. Pruébelo, por ejemplo, con la bandera de Stack Overflow o comparta diálogos o con uno de los menús desplegables de la barra de menú superior de su navegador. 2 de abril de 2017 a las 8:42
  • ReactDOM.findDOMNodey la cadena refestán en desuso, deberían usar refdevoluciones de llamada: github.com/yannickcr/eslint-plugin-react/issues/…
    dain
    1 de marzo de 2018 a las 13:09
  • esta es la solución más simple y funciona perfectamente para mí incluso cuando la adjunto al document
    Flion
    7 de marzo de 2018 a las 23:47
13

Alternativamente:

const onClickOutsideListener = () => {
    alert("click outside")
    document.removeEventListener("click", onClickOutsideListener)
  }

...

return (
  <div
    onMouseLeave={() => {
          document.addEventListener("click", onClickOutsideListener)
        }}
  >
   ...
  </div>
2
  • La respuesta más aceptada 10 de mayo a las 15:45
  • simple y me funciona perfectamente
    Eqzt111
    23 de julio a las 9:55
11

Material-UI tiene un pequeño componente para resolver este problema: https://material-ui.com/components/click-away-listener/ que puede elegir. Tiene un peso de 1,5 kB comprimido en gzip, es compatible con móviles, IE 11 y portales.

10

El camino Ez ...

const componentRef = useRef();

useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
    function handleClick(e: any) {
        if(componentRef && componentRef.current){
            const ref: any = componentRef.current
            if(!ref.contains(e.target)){
                // put your action here
            }
        }
    }
}, []);

luego pon la referencia en tu componente

<div ref={componentRef as any}> My Component </div>
2
  • Esto funcionó para mí con mucho menos código. TY!
    Joe
    26 feb a las 22:58
  • Con esto no necesito ningún paquete ane react. Gracias hermano.
    dhidy
    1 de mayo a las 15:39
5

Para aquellos que necesitan un posicionamiento absoluto, una opción simple por la que opté es agregar un componente de envoltura que tenga un estilo que cubra toda la página con un fondo transparente. Luego puede agregar un onClick en este elemento para cerrar su componente interno.

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

Como está ahora, si agrega un controlador de clic en el contenido, el evento también se propagará al div superior y, por lo tanto, activará el controladorOutsideClick. Si este no es su comportamiento deseado, simplemente detenga la propagación de eventos en su controlador.

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

'

4
  • El problema con este enfoque es que no puede tener ningún clic en el Contenido; el div recibirá el clic en su lugar.
    rbonick
    1 de junio de 2017 a las 22:12
  • Dado que el contenido está en el div, si no detiene la propagación del evento, ambos recibirán el clic. Este suele ser un comportamiento deseado, pero si no desea que el clic se propague al div, simplemente detenga eventPropagation en el controlador onClick de su contenido. He actualizado mi respuesta para mostrar cómo. 2 de junio de 2017 a las 23:45
  • Sin embargo, esto no le permitirá interactuar con otros elementos de su página, ya que estarán cubiertos por el div contenedor.
    whirish
    10 de enero a las 2:33
  • 2
    Esta solución debería recibir más votos a favor. Una de sus principales ventajas, más allá de su fácil implementación, es que deshabilita la interacción con otro elemento. Este es el comportamiento predeterminado de la alerta y el menú desplegable nativos y, por lo tanto, debe usarse para todas las implementaciones personalizadas del menú desplegable y modal. 2 de febrero a las 10:19
5

Esto ya tiene muchas respuestas, pero no abordan e.stopPropagation()y evitan hacer clic en los enlaces de reacción fuera del elemento que desea cerrar.

Debido al hecho de que React tiene su propio controlador de eventos artificial, no puede usar document como base para los oyentes de eventos. Debe hacerlo e.stopPropagation()antes de esto, ya que React usa el documento en sí. Si usa, por ejemplo, en su document.querySelector('body')lugar. Puede evitar el clic del enlace React. A continuación se muestra un ejemplo de cómo implemento hacer clic afuera y cerrar.
Esto usa ES6 y 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;
5
import { useClickAway } from "react-use";

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

Hice esto en parte siguiendo esto y siguiendo los documentos oficiales de React sobre el manejo de referencias que requieren reaccionar ^ 16.3. Esto es lo único que funcionó para mí después de probar algunas de las otras sugerencias aquí ...

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 => {
    /*Validating click is made inside a component*/
    if ( this.inputRef.current === e.target ) {
      return;
    }
    this.handleclickOutside();
  };
  handleClickOutside(){
    /*code to handle what to do when clicked outside*/
  }
  render(){
    return(
      <div>
        <span ref={this.inputRef} />
      </div>
    )
  }
}
1
  • Corrija esto this.handleclickOutside (); debería ser this.handleClickOutside () 15 de agosto de 2020 a las 8:13
4

Aquí está mi enfoque (demostración - https://jsfiddle.net/agymay93/4/ ):

He creado un componente especial llamado WatchClickOutsidey se puede usar como (supongo que JSXsintaxis):

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

Aquí está el código del WatchClickOutsidecomponente:

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>
    );
  }
}
3
  • Gran solución: para mi uso, cambié para escuchar el documento en lugar del cuerpo en el caso de páginas con contenido corto, y cambié ref de string a dom reference: jsfiddle.net/agymay93/9 9 de mayo de 2017 a las 11:58
  • y usó span en lugar de div, ya que es menos probable que cambie el diseño, y se agregó una demostración de manejo de clics fuera del cuerpo: jsfiddle.net/agymay93/10 9 de mayo de 2017 a las 12:06
  • La cadena refestá en desuso, debería usar refdevoluciones de llamada: reactjs.org/docs/refs-and-the-dom.html
    dain
    1 de marzo de 2018 a las 13:10
3

Para ampliar la respuesta aceptada hecha por Ben Bud , si está utilizando componentes con estilo, pasar refs de esa manera le dará un error como "this.wrapperRef.contains no es una función".

La solución sugerida, en los comentarios, para envolver el componente con estilo con un div y pasar la referencia allí, funciona. Dicho esto, en sus documentos ya explican la razón de esto y el uso adecuado de las referencias dentro de los componentes con estilo:

Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. It's not possible to call DOM methods, like focus, on our wrappers directly. To get a ref to the actual, wrapped DOM node, pass the callback to the innerRef prop instead.

Al igual que:

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

Luego puede acceder a él directamente dentro de la función "handleClickOutside":

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

Esto también se aplica al enfoque "onBlur":

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

Mi mayor preocupación con todas las otras respuestas es tener que filtrar los eventos de clic desde la raíz / padre hacia abajo. Encontré que la forma más fácil era simplemente establecer un elemento hermano con posición: fijo, un índice z 1 detrás del menú desplegable y manejar el evento de clic en el elemento fijo dentro del mismo componente. Mantiene todo centralizado en un componente determinado.

Código de ejemplo

#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
  • 1
    Mmmm ingenioso. ¿Funcionaría con varias instancias? ¿O cada elemento fijo podría bloquear potencialmente el clic de los demás? 11 de mayo de 2017 a las 14:40
2
componentWillMount(){

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

handleClickOutside(event) {

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

El evento path[0]es el último elemento en el que se hace clic

1
  • 3
    Asegúrese de eliminar el detector de eventos cuando el componente se desmonta para evitar problemas de administración de memoria, etc.
    agm1984
    5 de ene. De 2018 a las 23:07
2

Usé este módulo (no tengo ninguna asociación con el autor)

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

Hizo bien el trabajo.

1
  • ¡Esto funciona fantásticamente! Mucho más simple que muchas otras respuestas aquí y a diferencia de al menos otras 3 soluciones aquí, donde para todas recibí que "Support for the experimental syntax 'classProperties' isn't currently enabled"esto funcionó directamente. Para cualquiera que pruebe esta solución, recuerde borrar cualquier código persistente que haya copiado al probar las otras soluciones. por ejemplo, agregar oyentes de eventos parece entrar en conflicto con esto. 18/10/18 a las 6:54
2

En mi caso DROPDOWN, la solución de Ben Bud funcionó bien, pero tenía un botón de alternancia separado con un controlador onClick. Por lo tanto, la lógica de clic exterior entró en conflicto con el botón del conmutador de clic. Así es como lo resolví pasando también la referencia del botón:

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>
  );
}
1
  • Respuesta muy útil. En el momento en que separé ambas referencias, la función onClickOutside no se activa cuando hago clic en cualquiera de los dos componentes 23 de junio a las 21:15
2

Texto mecanografiado con ganchos

Nota: Estoy usando React versión 16.3, con React.createRef. Para otras versiones, use la devolución de llamada ref.

Componente desplegable:

interface DropdownProps {
 ...
};

export const Dropdown: React.FC<DropdownProps> () {
  const ref: React.RefObject<HTMLDivElement> = React.createRef();
  
  const handleClickOutside = (event: MouseEvent) => {
    if (ref && ref !== null) {
      const cur = ref.current;
      if (cur && !cur.contains(event.target as Node)) {
        // close all dropdowns
      }
    }
  }

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

  return (
    <div ref={ref}>
        ...
    </div>
  );
}

0
1

Un ejemplo con estrategia

Me gustan las soluciones proporcionadas que suelen hacer lo mismo mediante la creación de un contenedor alrededor del componente.

Dado que esto es más un comportamiento, pensé en Estrategia y se me ocurrió lo siguiente.

Soy nuevo en React y necesito un poco de ayuda para guardar un texto estándar en los casos de uso

Por favor revisa y dime lo que piensas.

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

Uso de la muestra

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() {...}
}

Notas

Pensé en almacenar los componentganchos del ciclo de vida pasados y anularlos con métodos similares a este:

const baseDidMount = component.componentDidMount;

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

componentes el componente pasado al constructor de ClickOutsideBehavior.
Esto eliminará el texto estándar de habilitar / deshabilitar del usuario de este comportamiento, pero no se ve muy bien.

1

Como alternativa a .contains, puede utilizar el método .closest. Cuando quiera verificar si un clic estuvo fuera del elemento con id = "apple", entonces puedo usar:

const isOutside = !e.target.closest("#apple");

Esto verifica si algún elemento en el árbol sobre el que se hizo clic tiene una identificación de "manzana". ¡Tenemos que negar el resultado!

1
  • la mejor solucion 7 oct.20 a las 19:58
1

Me gusta la respuesta de @Ben Bud, pero cuando hay elementos visualmente anidados, contains(event.target)no funciona como se esperaba.

Entonces, a veces es mejor calcular que el punto en el que se hizo clic está visualmente dentro del elemento o no.

Aquí está mi código de React Hook para la situación.

import { useEffect } from 'react'

export function useOnClickRectOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      const targetEl = ref.current
      if (targetEl) {
        const clickedX = event.clientX
        const clickedY = event.clientY
        const rect = targetEl.getBoundingClientRect()
        const targetElTop = rect.top
        const targetElBottom = rect.top + rect.height
        const targetElLeft = rect.left
        const targetElRight = rect.left + rect.width

        if (
          // check X Coordinate
          targetElLeft < clickedX &&
          clickedX < targetElRight &&
          // check Y Coordinate
          targetElTop < clickedY &&
          clickedY < targetElBottom
        ) {
          return
        }

        // trigger event when the clickedX,Y is outside of the targetEl
        handler(event)
      }
    }

    document.addEventListener('mousedown', listener)
    document.addEventListener('touchstart', listener)

    return () => {
      document.removeEventListener('mousedown', listener)
      document.removeEventListener('touchstart', listener)
    }
  }, [ref, handler])
}

0

Encontré esto en el artículo a continuación:

render () {return ({this.node = node;}}> Toggle Popover {this.state.popupVisible && (¡Soy un popover!)}); }}

Aquí hay un gran artículo sobre este tema: "Manejar clics fuera de los componentes de React" https://larsgraubner.com/handle-outside-clicks-react/

1
  • ¡Bienvenido a Stack Overflow! Las respuestas que son solo un enlace generalmente están mal vistas: meta.stackoverflow.com/questions/251006/… . En el futuro, incluya una cita o un ejemplo de código con la información relevante para la pregunta. ¡Salud! 6 de abril de 2018 a las 4:02
0

UseOnClickOutside Hook - React 16.8 +

Crear una función useOnOutsideClick general

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, []);

Luego use el gancho en cualquier componente funcional.

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

};

Enlace a la demostración

Parcialmente inspirado por la respuesta de @ pau1fitzgerald.

0

Si desea utilizar un componente pequeño (466 Byte comprimido en gzip) que ya existe para esta funcionalidad, puede consultar esta biblioteca react-outclick .

Lo bueno de la biblioteca es que también te permite detectar clics fuera de un componente y dentro de otro. También admite la detección de otros tipos de eventos.

0

Si desea utilizar un componente pequeño (466 Byte comprimido en gzip) que ya existe para esta funcionalidad, puede consultar esta biblioteca react-outclick . Captura eventos fuera de un componente react o elemento jsx.

Lo bueno de la biblioteca es que también te permite detectar clics fuera de un componente y dentro de otro. También admite la detección de otros tipos de eventos.

0

Sé que esta es una pregunta antigua, pero sigo encontrándome con esto y tuve muchos problemas para resolver esto en un formato simple. Entonces, si esto haría la vida de alguien un poco más fácil, use OutsideClickHandler de airbnb. Es el complemento más simple para realizar esta tarea sin escribir su propio código.

Ejemplo:

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