Artículo original escrito por: Mariya Diminsky
Artículo original: Event Bubbling and Event Catching in JavaScript and React – A Beginner's Guide
Traducido y adaptado por: Cristian Sulbaran

En este artículo, te ayudaremos a comprender la generación/propagación de eventos (event bubbling) y la captura de eventos (event catching) como un profesional. Este recurso se creó para ayudarte a comprender la propagación de eventos y cómo funciona en JavaScript y React de una manera clara y comprensible. ❤

Una vez que hayas pasado por esta introducción completa a la generación de eventos y al almacenamiento en caché de eventos, deberías poder comenzar a aplicar lo que has aprendido aquí en tus proyectos de inmediato.

Esto es lo que aprenderás:

¿Qué es la delegación de eventos?

En pocas palabras, la delegación de eventos es simplemente una poderosa técnica de JavaScript que permite un manejo de eventos más eficiente.

? Pros (más tarde)

Esta técnica generalmente se considera eficaz, ya que solo se utiliza una función de escucha de eventos en el padre de nivel superior en lugar de una para cada elemento hijo.

? Contras (más tarde)

Una vez que se llama al evento de un elemento secundario interno, todos los elementos arriba / abajo también serán llamados (propagación / captura). Para evitar que esto suceda, se debe llamar a un método en el objeto event.

La propagación y la captura (explicado más adelante) nos permiten implementar el patrón de delegación de eventos.

¿Qué es la propagación de eventos?

Supongamos que conocemos a una chica llamada Molly, que tampoco es una persona real, sino que es, ? redoble de tambores, un componente de React. Wow - qué conveniencia.

shiba inu meme "wow such convenience. much impress. so wow"
generated via https://memegenerator.net/

Tiene un div padre único con un controlador de eventos onClick que, cuando se hace clic, llama a todos a la mesa para comer su comida.

Dentro de este div principal hay varios elementos tipo button que, cuando se hace clic, crean un elemento de comida simulado (es decir, el de console.log).

import React, { Component } from "react";

class Molly extends Component {
    llamarFamiliaAComer() {
        console.log("¡Familia!¡La comida está lista!");
    }

    cocinarHuevos() {
        console.log("Molly está cocinando huevos esponjosos...");
    }

    hacerArroz() {
        console.log("Molly está haciendo un delicioso arroz jazmín...");
    }

    mezclarPollo() {
        console.log("¡Molly está mezclando pollo con una deliciosa salsa picante!");
    }

    render() {
        return (
            <div className="soy-un-padre" onClick={this.llamarFamiliaAComer}>
                <button className="soy-un-hijo" onClick={this.cocinarHuevos}>Cocinar Huevos</button>
                <button className="soy-un-hijo" onClick={this.hacerArroz}>Hacer Arroz</button>
                <button className="soy-un-hijo" onClick={this.mezclarPollo}>Mezclar Pollo</button>
            </div>
        );
    }

}

export default Molly;

Y esto es lo que sucede cuando haces clic en cada uno(en inglés):

Aquí hay una pequeña versión en codepen si deseas seguir este camino:

Generación/propagación de eventos JS para fcc en español
¡No te olvides de abrir la consola para ver los resultados!

Como puedes ver, esto le sucede a todos los hijos:

  1. Primero, se activa el controlador de eventos del button.
  2. En segundo lugar, se activa el controlador de eventos del div padre.

En la mayoría de los casos, probablemente desees que solo se llame al controlador de eventos del botón cuando haga clic en él. ¡Pero como puedes ver, el evento de los padres también se activa ...!?

Esto se llama ✨Propagación de eventos✨ (Event Bubbling).
En las próximas secciones, discutiremos qué diablos está sucediendo y cómo podemos solucionarlo.

¿Cómo ocurre la propagación de eventos en JavaScript?

¿Por qué existe la propagación de eventos?

Una de las intenciones de JavaScript con la creación del patrón de propagación de eventos era facilitar la captura de eventos de una fuente, el elemento padre, en lugar de configurar un controlador de eventos en cada elemento secundario interno.

Orden de activación de propagación de eventos

Hay tres fases por las que pasa la propagación de eventos

image-20-1
Imagen de https://ehsankorhani.com/ y traducida.
  1. ? Fase de captura: es la primera fase en la que se activa un evento. Este evento "captura" o se propaga primero a través del evento padre, que es el objeto de window, luego el document, luego el html y luego los demás elementos internos. Baja hasta que llega al event.target (en lo que hiciste clic / el evento desencadenado).
  2. ? Fase objetiva: la segunda fase es cuando llegamos al event.target . Por ejemplo, cuando un usuario hace clic en un botón, este es el elemento actual de botón.
  3. ? Fase de propagación: la tercera fase. Este evento comienza desde event.target y se propaga hasta que llega al elemento padre superior nuevamente (aunque el evento del elemento padre no se vuelve a llamar).

Ten en cuenta que, si bien hay 3 fases principales, la fase objetiva en realidad no se maneja por separado. Los controladores de eventos en las fases de Captura y Generación se activan aquí.

También existe técnicamente otra fase denominada "Fase Ninguna", en la que no se está produciendo ninguna fase de eventos. Puedes acceder a la fase en la que se encuentra un elemento a través de event.eventPhase.

Teniendo en cuenta lo que acabas de aprender, observa el siguiente ejemplo.

Supongamos que un usuario hizo clic en un elemento td en una table. ¿Cómo ocurriría la propagación de eventos aquí? ? Tómate un momento para pensarlo.

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  </head>
  <body>
    <div id="root">
      <table>
        <tbody>
          <tr>
            <td>Shady Grove</td>
            <td>Aeolian</td>
          </tr>
          <tr>
            <td>Over the River, Charlie</td>
            <td>Dorian</td>
          </tr>
        </tbody>
      </table>
    </div>
  </body>
</html>

Esto es lo que realmente está sucediendo, en el mismo orden que acabamos de mencionar:

Ten en cuenta que el DefaultView, sería el objeto Window.

another chart displaying event propagation in more detail
Imagen de https://www.w3.org/

¿Cómo ocurre la propagación de eventos en React?

React, por otro lado, ha creado algo llamado SyntheticEvent.

Estos son simplemente envoltorios para el objeto event del navegador. El caso de uso básico es similar e incluye métodos como stopPropagation y preventDefault (que discutiremos más adelante). El mayor beneficio es que funcionan de la misma manera en todos los navegadores.

React no adjunta controladores de eventos a los nodos, sino a la raíz del documento. Cuando se activa un evento, React llama primero al elemento adecuado (es decir, la fase objetiva, elemento en el que hiciste clic) y luego comienza a generar.

¿Por qué React hace esto en lugar de simplemente manejar eventos de manera similar al DOM nativo?

Consistencia del navegador

Es importante que los eventos funcionen igual en todos los navegadores. React creó eventos sintéticos para asegurarse de que las propiedades sigan siendo consistentes en diferentes navegadores y plataformas.

No querrás crear una aplicación cuando un evento funciona en un navegador, pero luego un usuario en un navegador diferente usa tu aplicación y ya no funciona; esa es una mala experiencia de usuario.

Desencadena desde el elemento que realmente quieres desencadenar

Donde se establece el controlador de eventos es donde la intención es llamarlo, en ese elemento en particular y en ningún otro lugar (estamos ignorando temporalmente algunos casos extremos aquí, por supuesto, para comprender primero el concepto básico).

Ese evento sabe más sobre el elemento en el que está configurado, por lo que debería ser el primero en activarse. Después de eso, a medida que la Propagación de Eventos asciende, cada elemento de arriba sabe cada vez menos.

Tomemos, por ejemplo, nuestro ejemplo anterior con nuestro componente Molly. Sabemos que la extrañas, así que aquí está de nuevo:

? ¿Notaste que cuando se hace clic en un botón, el controlador de eventos de ese botón se llama primero y solo entonces se llama al controlador de eventos principal?

Nunca sucede a la inversa (es decir, la fase de captura nunca se activa).
Esto se debe a que el Evento Sintético de React solo usa la fase de propagación(la fase objetivo se incluye aquí). Esto tiene sentido si la intención es enfocarse en el event.target (el botón en este ejemplo) que activó el evento primero.

Ten en cuenta que React solo está simulando la fase de captura y propagación nativa de JavaScript con estos Eventos Sintéticos, por lo que puedes notar algunas diferencias a medida que pasa el tiempo (se explica más adelante en este artículo).

⚠️ El Evento Sintético no se centra de forma nativa en la fase de captura a menos que lo establezca específicamente. Para que la Fase de Captura se desencadene, simplemente configura el controlador de eventos onClick del div padre a onClickCapture:

import React, { Component } from "react";

class Molly extends Component {
    ...

    render() {
        return (
            <div className="soy-un-padre" onClickCapture={this.llamarFamiliaAComer}>
                <button className="soy-un-hijo" onClick={this.cocinarHuevos}>Cocinar Huevos</button>
                <button className="soy-un-hijo" onClick={this.hacerArroz}>Hacer Arroz</button>
                <button className="soy-un-hijo" onClick={this.mezclarPollo}>Mezclar Pollo</button>
            </div>
        );
    }

}

export default Molly;

Ten en cuenta que en lugar de la fase de propagación, la fase de captura se activa a continuación

⚠️Por último, queríamos mencionar que en React Versión 16 y versiones anteriores, cuando se activa la fase de propagación en SyntheticEvents, actúa de manera similar a la fase de propagación nativa de JavaScript al adjuntar controladores de eventos hasta el Document.

Ahora, en React Versión 17+, los controladores de eventos solo alcanzan el elemento root

Image displaying React's bubbling phase ending at the root level in React Version 17 but it ends at Window/Document in React Version 16 or lower
Imagen de React

¿Cómo detener la propagación de eventos en tus componentes?

Ahora que comprendes los conceptos básicos de la Propagación de Eventos, la Generación de Eventos y la Captura de Eventos, analicemos cómo solucionar nuestro problema inicial.

Tienes un botón (o algún otro elemento) y deseas que solo se active el controlador de eventos del botón; no se debe activar ningún otro padre.

? Entonces, ¿cómo podemos evitar que esto suceda? Tienes pocas opciones:

event.stopPropagation()

Esto evitará que se active el evento de cualquier componente principal. Para usar esto:

  1. Asegúrate de pasar el objeto de event como parámetro.
  2. Utiliza el método stopPropagation en el objeto de evento sobre tu código dentro de tu función de controlador de eventos.

Ten en cuenta que cambiamos del onClickCapture al onClick del div padre nuevamente

import React, { Component } from "react";

class Molly extends Component {
    llamarFamiliaAComer() {
        console.log("¡Familia!¡La comida está lista!");
    }

    cocinarHuevos() {
        event.stopPropagation(); // USADO AQUÍ!
        console.log("Molly está cocinando huevos esponjosos...");
    }

    hacerArroz() {
        console.log("Molly está haciendo un delicioso arroz jazmín...");
    }

    mezclarPollo() {
        console.log("¡Molly está mezclando pollo con una deliciosa salsa picante!");
    }

    render() {
        return (
            <div className="soy-un-padre" onClick={this.llamarFamiliaAComer}>
                <button className="soy-un-hijo" onClick={this.cocinarHuevos}>Cocinar Huevos</button>
                <button className="soy-un-hijo" onClick={this.hacerArroz}>Hacer Arroz</button>
                <button className="soy-un-hijo" onClick={this.mezclarPollo}>Mezclar Pollo</button>
            </div>
        );
    }

}

export default Molly;

Arriba solo agregamos stopPropagation a la función cocinarHuevos. Entonces, cuando se hace clic en el botón Cocinar Huevos, solo se activa el evento solo para ese elemento.

event.stopImmediatePropagation()

Supongamos que tienes varios eventos en el mismo elemento. Si usas event.stopPropagation(), asegúrate de que detendrá la activación de cualquier evento principal. Pero si tienes varios eventos en el mismo elemento, todos se dispararán.

Para evitar que se activen otros eventos en el mismo elemento, utiliza event.stopImmediatePropagation() en su lugar. Evitará que se activen los eventos de ambos padres y del mismo elemento.

Si te encuentras en una situación en la que event.stopPropagation() no te funciona, prueba event.stopImmediatePropagation() en su lugar.

Nota: De vez en cuando, puede haber una biblioteca de terceros en tu aplicación que hace que la primera no funcione. Por supuesto, sería una buena idea ver qué causó que este último funcionara, pero no el primero, y podríamos darte otra pista para solucionar el problema.

event.preventDefault()

Dependiendo del elemento y controlador de eventos, es posible que desees utilizar esto.

Por ejemplo:
Si tienes un formulario y no deseas que la página se actualice cuando se envíe. Estás configurando tu propia funcionalidad de ruta y no quieres que la página se actualice.

Event.target vs Event.currentTarget

Comprender la diferencia entre estas dos propiedades de destino en el objeto event realmente puede ahorrarte un dolor de cabeza en el futuro.

Recuerda: el elemento que desencadena el evento no siempre es el mismo que el elemento que tiene adjunto el detector de eventos(listener).

? ¿Confundido? No te preocupes, repasemos esto juntos.

Tomemos nuestro ejemplo anterior y hagamos un console.log tanto el event.target como el event.currentTarget dentro del controlador de eventos del div padre.

import React, { Component } from "react";

class Molly extends Component {
    //CHEQUEANDO AL PADRE
    llamarFamiliaAComer() {
        console.log("¡Familia!¡La comida está lista!");

        console.log("event.target:", event.target);
        console.log("event.currentTarget", event.currentTarget);
    }
    
    ...

    render() {
        return (
            <div className="soy-un-padre" onClick={this.llamarFamiliaAComer}>
                <button className="soy-un-hijo" onClick={this.cocinarHuevos}>Cocinar Huevos</button>
                <button className="soy-un-hijo" onClick={this.hacerArroz}>Hacer Arroz</button>
                <button className="soy-un-hijo" onClick={this.mezclarPollo}>Mezclar Pollo</button>
            </div>
        );
    }

}

export default Molly;

Ahora, cuando hacemos clic en el botón cocinarHuevos, ¿qué vemos?

event-currentTarget-ejemplo-espanol-1
Imagen por Mariya Diminsky del artículo original(editada)

Observa que el controlador de eventos del div padre es consciente de que el target (objetivo) previsto es el botón.

Pero como estamos comprobando dentro del controlador de eventos del padre, vemos que el div padre es el currentTarget(objetivo actual).

Ok, veamos más sobre esto.

¿Qué pasa si tomamos los mismos console.logs y verificamos dentro del controlador de eventos del botón actual?

? ¿Qué veríamos ahora?

import React, { Component } from "react";

class Molly extends Component {
    llamarFamiliaAComer() {
        console.log("¡Familia!¡La comida está lista!");
    }
    //CHEQUEANDO AL HIJO
    cocinarHuevos() {
        console.log("Molly está cocinando huevos esponjosos...");

        console.log("event.target:", event.target);
        console.log("event.currentTarget", event.currentTarget);
    }
    ...

    render() {
        return (
            <div className="soy-un-padre" onClick={this.llamarFamiliaAComer}>
                <button className="soy-un-hijo" onClick={this.cocinarHuevos}>Cocinar Huevos</button>
                <button className="soy-un-hijo" onClick={this.hacerArroz}>Hacer Arroz</button>
                <button className="soy-un-hijo" onClick={this.mezclarPollo}>Mezclar Pollo</button>
            </div>
        );
    }

}

export default Molly;
event-currentTarget-ejemplo-2-espanol
Imagen por Mariya Diminsky del artículo original(editada)

Ten en cuenta que, dado que ahora estamos comprobando el interior del controlador de eventos del botón, vemos que el currentTarget ha cambiado al botón.

Y, por supuesto, dado que estamos haciendo clic en el botón, ya sabemos que el target será el botón.

Teniendo en cuenta lo que acabas de aprender, ahora sabes que:

  • event.target es el elemento más profundamente anidado que causó el evento.
  • event.currentTarget es el elemento que escucha el evento (donde se adjunta el detector de eventos(listener)).

Orden de activación de eventos actualizada y parámetro useCapture en JavaScript

En JavaScript, EventTarget.addEventListener se usará para agregar un controlador a un evento.

Cuando echamos un vistazo a los documentos de MDN, vemos que puedes configurar la capture opcionalmente dentro del objeto options o mediante el parámetro useCapture (ahora también opcional), que hace lo mismo.

// Ahora puedes hacer esto:
tuElemento.addEventListener(tipo, detector, { capture: true });

// o esto:
tuElemento.addEventListener(tipo, detector, useCapture: true);

⚠️ La razón de esto es que, a menos que lo establezcamos específicamente, la Fase de Captura se ignorará y, en cambio, solo la Fase de Propagación(después de la Fase Objetivo) se activará de forma nativa en JavaScript. MDN también lo explica:

Para los detectores de eventos adjuntos al objetivo del evento, el evento está en la Fase de Objetivo, en lugar de las Fases de Captura y Propagación. Los detectores de eventos en la Fase de "Captura" se llaman antes que los detectores de eventos en cualquier fase de no captura.

Ten en cuenta que el parámetro useCapture no siempre ha sido opcional en los navegadores más antiguos. Asegúrate de consultar caniuse.com antes de implementarlo.

¿Qué eventos no propagan y cómo se manejan?

Aunque la mayoría de los eventos propagan, ¿sabías que varios no lo hacen?

A continuación, se muestran algunos ejemplos en JavaScript nativo:

⚠️ Los eventos que propagan se establecen como true en la opción de bubbles cuando se crea el evento, aunque todavía pasan por la Fase de Captura.

Event Listeners en React Versión 16 y antes de VS Versión 17+

Como aprendimos, SyntheticEvent de React no siempre actúa de la misma manera que sus equivalentes nativos de JavaScript.

Conozcamos algunas de estas diferencias, así como los cambios realizados entre las versiones de React.

Eventos que no esperarías que propagaran en React.

Por ejemplo, esperarías que onBlur y onFocus de React no propaguen, ya que el equivalente nativo de JavaScript no lo hace, ¿correcto? Sin embargo, React ha hecho que estos eventos, entre otros, sigan propagando intencionalmente.

⚠️ Si bien React v17 ha realizado algunos cambios en ciertos eventos como onScroll, que ya no propaga, la mayoría de los eventos aún continúan propagando.

Consulta esta respuesta y este artículo para obtener más detalles sobre este tema.

event.target.value solía ser nulo en funciones asíncronas

Antes de React v17, si intentabas acceder a un evento en una función asincrónica, notarías que no estaría definido.

Esto se debe a que los objetos SyntheticEvent de React se agruparon, lo que significa que después de llamar a los controladores de eventos, ya no tendrías acceso a ellos, ya que se restablecerían y volverían a poner en el grupo.

image-25
Imagen de React

Esto causa problemas para las funciones asíncronas que necesitan acceder a la información dentro de ese evento en un momento posterior.

⚠️ La única forma de conservar esta información dentro de las funciones asíncronas era llamar event.persist():

image-26
Imagen de React

La intención de esto era mejorar el rendimiento. Pero después de una inspección más cercana, el equipo de React descubrió que solo confundía a los desarrolladores y, en realidad, no mejoraba mucho el rendimiento, por lo que se eliminó por completo.

⚠️ Con el lanzamiento de React v17, React ya no agrupa objetos SyntheticEvent. Por lo tanto, puedes esperar recibir el event.target.value deseado dentro de tus funciones asíncronas sin necesidad del event.persist().

Asegúrese de leer más sobre esta actualización aquí.

Caso Borde Especial: ¿Qué pasa si necesitas que un elemento padre externo dispare también?

¡Tomemos todo lo que aprendimos y arreglemos un caso especial para que puedas aplicarlo en tu próxima (o actual) aplicación React!

? Digamos que queremos que ambos funcionen en nuestra aplicación:

  1. Cuando un usuario hace clic en el elemento div/button/etc interno, queremos que ese evento se active solo (o en nuestro ejemplo a continuación, cambiando de canal en el televisor).
  2. Cuando un usuario hace clic en el div principal externo, el evento principal se activa (esto podría ser útil para un modal emergente. Cuando un usuario hace clic fuera del modal, deseas que se cierre la ventana emergente, o en nuestro ejemplo a continuación, que un televisor se apague).

Actualmente, sabes que si haces clic en el elemento principal / secundario, el sistema SyntheticEvent de React activará la propagación.

También sabes que para detener esto podemos usar event.stopPropagation().

Pero nos quedamos con un dilema.

¿Qué sucede si deseas que un controlador de eventos se active en una situación (nuestro #1) y otro controlador de eventos se active en otra situación (#2)?

⚠️ Si usamos event.stopPropagation(), detendría la activación de un controlador de eventos, pero nunca podrás llamar al otro controlador de eventos en otra situación. ¿Cómo podemos arreglar esto?

Para resolver este problema, utilicemos el patrón de estado de React.

Ten en cuenta que estamos utilizando funciones de flecha aquí, por lo que el bind no es necesario. Si no estás seguro de lo que esto significa, no dudes en leer otro artículo que escribió Mariya sobre este tema aquí(en inglés).

ℹ️ A continuación, hemos incluido una versión de React Class Component y una versión de React Hooks; usa la que prefieras. Asegúrate de leer los comentarios detenidamente:

import React, { Fragment, Component } from "react";

import "./TV.css" // puedes ignorar esto ya que esto no existirá de tu parte

class TV extends Component {
    state = { canal: 1, deberiaApagarTv: false };

  	// el div principal se activa si la TV está APAGADA al hacer clic en 
	// cambiarCanal o apagarTV no se activará al mismo tiempo 
	//debido al event.stopPropagation() aquí
    prenderTv = (event) => {
        console.log("En prenderTv");

        const { deberiaApagarTv } = this.state;

        if (deberiaApagarTv) {
            event.stopPropagation();

            // Restablecimos el canal en 1, pero puedes hacer lo que necesites aquí
            this.setState({ deberiaApagarTv: false, canal: 1 });
        }
    }

    // el botón hijo cambiarCanal se activa si TV está ENCENDIDA(ON) 
    // haciendo clic en el div padre, o apagarTV no se activará al mismo tiempo 
    // debido al event.stopPropagation() aquí
    cambiarCanal = (event) => {
        console.log("En cambiarCanal");

        const { canal, deberiaApagarTv } = this.state;

        if (!deberiaApagarTv) {
            event.stopPropagation();

            // Incrementamos el canal en 1, pero puedes hacer lo que necesites aquí
            this.setState({ canal: canal + 1 });
        }
    }

    // el botón apagarTV se activa 
    // haciendo clic en el div principal o cambiar el canal no se activará al mismo tiempo 
    // debido al event.stopPropagation() aquí
    apagarTV = (event) => {
        console.log("En apagarTV");

        event.stopPropagation();

        this.setState({ deberiaApagarTv: true });
    }

    ponerCanal = () => {
        const { canal, deberiaApagarTv } = this.state;

        if (deberiaApagarTv) {
            return (
                <div>Ya está, ¡No mas TV!</div>
            )
        }

        return (
            <Fragment>
                <div>Canal Actual: {canal}</div>
                <button className="soy-un-boton-hijo" onClick={this.apagarTV}>Apagar la TV</button>
            </Fragment>
        )
    }

    render() {
        const { deberiaApagarTv } = this.state;
        return (
            <div className="soy-un-padre" onClick={this.prenderTv}> 
                {this.ponerCanal()}
                <hr />
                <button 
                    disabled={deberiaApagarTv}
                    className="soy-un-boton-hijo" 
                    onClick={this.cambiarCanal}
                >
                    Cambiar canal
                </button>
            </div>
        );
    }

}

export default TV;
Ejemplo escrito como un componente de clase
import React, { Fragment, useState } from "react";

import "./TV.css" // puedes ignorar esto ya que esto no existirá de tu parte

const TV = () => {
    const [canal, setCanal] = useState(1);
    const [deberiaApagarCanal, setApagarTV] = useState(false);

    // el div padre se activa si la TV está APAGADA 
    // al hacer clic en cambiarCanal o apagarTV no se activará 
    // al mismo tiempo debido al event.stopPropagation() aquí
    const prenderTV = (event) => {
        console.log("En prenderTV");

        if (deberiaApagarCanal) {
            event.stopPropagation();

            // Restablecimos el canal en 1, pero puedes hacer lo que necesites aquí
            setApagarTV(false);
            setCanal(1);
        }
    }

    // el botón de cambiarCanal hijo se activa si la TV está ENCENDIDA 
    // haciendo clic en el div padre, o apagarTV 
    // no se activará al mismo tiempo debido al event.stopPropagation()) aquí
    const cambiarCanal = (event) => {
        console.log("En cambiarCanal");

        if (!deberiaApagarCanal) {
            event.stopPropagation();

            // Incrementamos el canal en 1, pero puedes hacer lo que necesites aquí
            setCanal(canal + 1);
        }
    }

    // se activa el botón de apagarTV
    // hacer clic en el div padre o cambiarCanal no se activará al mismo tiempo
    // debido al event.stopPropagation() aquí
    const apagarTV = (event) => {
        console.log("En apagarTV");

        event.stopPropagation();

        setApagarTV(true);
    }

    const ponerCanal = () => {
        if (deberiaApagarCanal) {
            return (
                <div>Ya está, ¡No mas TV!</div>
            )
        }

        return (
            <Fragment>
                <div>Canal Actual: {canal}</div>
                <button className="soy-un-boton-hijo" onClick={apagarTV}>Apagar la TV</button>
            </Fragment>
        )
    }

    return (
        <div className="soy-un-padre" onClick={prenderTV}> 
            {ponerCanal()}
            <hr />
            <button 
                disabled={deberiaApagarCanal}
                className="soy-un-boton-hijo" 
                onClick={cambiarCanal}
            >
                Cambiar canal
            </button>
        </div>
    );

}

export default TV;
Ejemplo escrito como un componente funcional utilizando React Hooks

? Y esto es lo que sucede cuando ejecutamos el código:

  1. Cuando hacemos clic en cambiarCanal, el canal se incrementa. Observa que los otros dos controladores de eventos no se ejecutan.
  2. Cuando hacemos clic en apagarTV, la interfaz de usuario cambia y si intentamos hacer clic en cualquier lugar fuera del div padre, los otros dos controladores de eventos no se ejecutan.
  3. Cuando hacemos clic dentro del div padre externo cuando el televisor está apagado, solo se ejecuta un controlador de eventos.

Ten en cuenta: en el ejemplo anterior, estamos usando state = {} en lugar del constructor() {...}. Esto se debe a que cuando Babel (un compilador de JavaScript) convierte tu código de React, escupe un constructor con todo dentro. Si sabes esto, no dudes en omitir la imagen a continuación:

image-27
Captura de pantalla de Mariya Diminsky tomada de Babel

Una solución aún más simple

Esa es una forma de hacerlo, ¡pero hay una solución aún más simple! Simplemente verifica dentro del controlador de eventos si el target(en lo que se hizo clic) es el mismo que el eventTarget(el controlador de eventos que escucha el evento).

Si es lo mismo, puedes llamar al stopPropagation. A continuación, se muestra un ejemplo rápido:

...

const Modal = ({ cabecera, contenido, textoBotonCancelar, textoBotonConfirmar, history, confirmar }) => {
    const cancelar = (event) => {
        detenerIntentoDePropagacion(event);

        // se hace algo acá
    }

    const botonConfirmar = (event) => {
        detenerIntentoDePropagacion(event);

        // se hace algo acá
    }
    
    // por lo que los elementos con múltiples controladores de eventos 
    // no se llaman innecesariamente más de una vez (es decir, propagación de SyntheticEvent)
    export const detenerIntentoDePropagacion = (event) => {
        if (event.target === event.currentTarget) {
            event.stopPropagation();
        }
    }

    return crearPortal(
        <div onClick={cancelar} className="ui dimmer modals visible active">
            <div className="ui tiny modal visible active">
                <div className="cabecera">{cabecera}</div>
                <div className="contenido">{contenido}</div>
                <div className="acciones">
                    <button onClick={cancelar} className="ui button">{textoBotonCancelar}</button>
                    <button onClick={botonConfirmar} className="ui red button">{textoBotonConfirmar}</button>
                </div>
            </div>
        </div>,
        document.getElementById("modal")
    );
}

¡Lo hiciste! ✨?✨

Has terminado este artículo y, con suerte, ahora comprendes la propagación y la captura de eventos como un profesional. ¡Hurra!

Ahora sabes:

  • Qué significa la delegación de eventos y cómo funcionan la propagación de eventos y la captura de eventos.
  • Cómo funciona la propagación de eventos de manera diferente en JavaScript y React.
  • Comprende mejor los beneficios y las advertencias del manejo de eventos en React.
  • Varios métodos que puedes utilizar para solucionar problemas que puedan surgir en tu caso particular
  • La diferencia entre Event.target y Event.currentTarget, así como que el evento desencadenado no siempre es el mismo que el que tiene el detector de eventos adjunto.
  • Cómo ocurre la propagación de eventos en JavaScript moderno y cómo usar el parámetro useCapture si necesitas usar la fase de captura.
  • Aprendimos que no todos los eventos propagan en JavaScript nativo, así como algunos de sus alias que sí lo hacen.
  • También aprendimos que casi todos los SyntheticEvents de React (aparte de algunas actualizaciones en React Versión 17) propagan.
  • Por último, ahora tienes una mejor comprensión sobre cómo manejar el caso borde de un elemento padre externo que necesita disparar sin detener otros controladores de eventos utilizando el estado de React

Más recursos / lectura adicional:

https://www.youtube.com/watch?v=Q6HAJ6bz7bY
https://javascript.info/bubbling-and-capturing
https://www.w3.org/TR/uievents/
https://chrisrng.svbtle.com/event-propagation-and-event-delegation
https://jsbin.com/hilome/edit?js,output

??¡Hola! ??‍? la autora original del artículo es Mariya Diminsky, una ingeniera de software autodidacta apasionada. Trabajó como ingeniera FullStacK, desarrolladora de frontend (ella ? React) y desarrolladora de Unity/C#. También es fundadora de TrinityMoon Studios y creador de The Girl Who Knew Time.

✨? Si disfrutaste de la lectura y te gustaría aprender más sobre varios temas de React/tópicos de System Design y más, considera seguirla en redes para obtener las últimas actualizaciones. ?