javascript - React, click outside event happens right after click to open, preventing the modal from showing - Stack Overflow

On button click I want the Modal to appear.The Modal ponent adds a eventListener so it closes when you

On button click I want the Modal to appear. The Modal ponent adds a eventListener so it closes when you click outside the modal. In React 18 the click event triggers because the button click that happend before Modal was rendered? If I change to react 17 this does not happen.

Find a CodeSandbox here. Notice, when you click the button the show state sets to true. Then the Modal ponent renders and calls the close function directly.

App.js:

import { useState } from "react";
import Modal from "./Modal";
import "./styles.css";

export default function App() {
  const [show, setShow] = useState(false);

  const close = () => {
    setShow(false);
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button
        onClick={(e) => {
          setShow(true);
        }}
      >
        Click
      </button>
      {show && <Modal close={close} />}
    </div>
  );
}

Modal.js

import "./styles.css";
import { useRef, useEffect } from "react";

export default function Modal({ close }) {
  const ref = useRef(null);

  useEffect(() => {
    const handleOutsideClick = (e) => {
      if (!ref?.current?.contains(e.target)) {
        console.log("This one gets called because of the button click", e);
        close();
      }
    };

    document.addEventListener("click", handleOutsideClick, false);
    return () => {
      document.removeEventListener("click", handleOutsideClick, false);
    };
  }, [close]);

  return (
    <div ref={ref} className="Modal">
      <h1>I'm a Modal!</h1>
    </div>
  );
}

On button click I want the Modal to appear. The Modal ponent adds a eventListener so it closes when you click outside the modal. In React 18 the click event triggers because the button click that happend before Modal was rendered? If I change to react 17 this does not happen.

Find a CodeSandbox here. Notice, when you click the button the show state sets to true. Then the Modal ponent renders and calls the close function directly.

App.js:

import { useState } from "react";
import Modal from "./Modal";
import "./styles.css";

export default function App() {
  const [show, setShow] = useState(false);

  const close = () => {
    setShow(false);
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button
        onClick={(e) => {
          setShow(true);
        }}
      >
        Click
      </button>
      {show && <Modal close={close} />}
    </div>
  );
}

Modal.js

import "./styles.css";
import { useRef, useEffect } from "react";

export default function Modal({ close }) {
  const ref = useRef(null);

  useEffect(() => {
    const handleOutsideClick = (e) => {
      if (!ref?.current?.contains(e.target)) {
        console.log("This one gets called because of the button click", e);
        close();
      }
    };

    document.addEventListener("click", handleOutsideClick, false);
    return () => {
      document.removeEventListener("click", handleOutsideClick, false);
    };
  }, [close]);

  return (
    <div ref={ref} className="Modal">
      <h1>I'm a Modal!</h1>
    </div>
  );
}
Share Improve this question edited Mar 19, 2023 at 11:57 Youssouf Oumar 46.1k16 gold badges100 silver badges104 bronze badges asked May 20, 2022 at 8:34 BennyBenny 8571 gold badge8 silver badges26 bronze badges 1
  • 1 Here is a reply from Dan Abramov: github./facebook/react/issues/24657#issuement-1150119055 – Lionel Commented Aug 17, 2022 at 11:15
Add a ment  | 

4 Answers 4

Reset to default 8

You can call event.stopPropagation to prevent multiple click handlers capturing the same click event.

    onClick={(e) => {
      e.stopPropagation();
      setShow(true);
    }}

I don't know why this would differ between React 17 and 18. React uses its own "synthetic events", and there might have been a change in how event propagation/bubbling happens between the two versions.

It might be connected to what's called "automatic batching" in React 18. https://github./reactwg/react-18/discussions/21

In your example, the Modal ponent uses native event handling with document.addEventlistener(). It seems that React 18 handles the click inside the app, which triggers a state change and a rerender, mounts the Modal ponent, runs the useEffect() hook, and creates the new event listener before the click event is propagated to the window node. In React 17, the event presumably finishes propagating before the re-render happens.

Dan Abramov gives some suggestions in this ment (https://github./facebook/react/issues/24657#issuement-1150119055).

Suggestion 1 is: In the useEffect, delay the addEventListener call with a setTimeout with 0 wait time. Example:

  useEffect(() => {
    const handleOutsideClick = (e) => {
      if (!ref?.current?.contains(e.target)) {
        console.log("This one gets called because of the button click", e);
        close();
      }
    };

    const timeoutId = setTimeout(() => {
      document.addEventListener("click", handleOutsideClick, false);
    }, 0);
    return () => {
      clearTimeout(timeoutId);
      document.removeEventListener("click", handleOutsideClick, false);
    };
  });

Suggestion 2 is to capture the current event and ignore just that one in the modal's useEffect. Example provided by @YoussoufOumar in this answer and at https://github./microsoft/fluentui/pull/18323

A different solution to the problem involves new startTransition API: https://codesandbox.io/s/gracious-benz-0ey6ol?file=/src/App.js:340-388

onClick={(e) => {
  startTransition(() => {
    setShow(true);
  })
}}

This new API (doc) definitely helps, but I'm not sure this is a legit solution, and docs are not quite clear on that. Would be nice if someone from React can ment -- is this an abuse?

You could change your if statement in handleOutsideClick as below. Yours is not working because the button for opening the Modal, since it's not inside the Modal, passes this if(!ref?.current?.contains(e.target)) you have.

Here is a working fork of your CodeSandbox. I'm passing the button's ref to Modal. And here is what I changed:

const handleOutsideClick = (e) => {
  if (!ref?.current?.contains(e.target) && !openButtonRef?.current?.contains(e.target)) {
    console.log("This one gets called because of the button click", e);
    close();
  }
};

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1740280214a4221277.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信