【问题标题】:React, click outside event fire right after the open event, preventing the modal from being displayedReact,在打开事件后立即单击外部事件触发,防止显示模式
【发布时间】:2022-11-10 15:00:17
【问题描述】:

我有个问题。我向一个元素添加了一个onClick 事件。它的事件处理程序的工作是改变一个状态。在该状态更改后,我会显示一个弹出窗口。我可以使用useRef 钩子访问该弹出窗口。然后我向document 添加一个click 事件,它的事件处理程序的工作是检查用户是否在弹出窗口之外单击。

但问题就在这里:当用户点击元素时,添加到document 的事件处理程序将立即被执行!但是怎么做?看看这些步骤,你会更好地理解我的观点:

用户单击显示弹出按钮-> onClick 事件处理程序已执行--> 状态已更改--> 添加到文档中的另一个事件(用于外部单击目的)--> 立即执行文档上单击事件的事件处理程序(当您单击一次显示弹出按钮时,所有这些都会发生!!)。

选项弹出组件:

import { useRef } from "react";
import useAxis from "../../hooks/useAxis";
import useOutSideClick from "../../hooks/useOutSideClick";

const Options = (props) => {
  const { children, setShowOptions, showOptions } = props;
  const boxRef = useRef();

  const { childrens, offset } = useAxis(children, {
    /* add an onClick to this components children(it has just one child and it is the open 
    popup button)*/
    onClick: () => {
      console.log("test1");
      //show the options popup
      setShowOptions(!showOptions);
    },
  });
  
  //close the popup if user clicked outside the popup
  useOutSideClick(boxRef, () => {
    console.log("test2");
    //close the popup
    setShowOptions((prevState) => !prevState);
  }, showOptions);

  return (
    <>
      {showOptions && (
        <div
          ref={boxRef}
          className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
          style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
        ></div>
      )}
      {childrens}
    </>
  );
};

export default Options;

useOutSideClick 自定义钩子:

import { useEffect } from "react";

//a custom hook to detect that user clicked
const useOutSideClick = (ref, outSideClickHandler, condition = true) => {
  useEffect(() => {
    if (condition) {
      const handleClickOutside = (event) => {
        console.log("hellloooo");
        //if ref.current doesnt contain event.target it means that user clicked outside
        if (ref.current && !ref.current.contains(event.target)) {
          outSideClickHandler();
        }
      };

      document.addEventListener("click", handleClickOutside);
      return () => {
        document.removeEventListener("click", handleClickOutside);
      };
    }
  }, [ref, condition]);
};

export default useOutSideClick;

使用Axios 自定义钩子:

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

const useAxis = (children, events) => {
  const childRef = useRef();
  const [offset, setOffset] = useState({
    x: "",
    y: "",
  });

  useEffect(() => {
    Object.keys(events).forEach((event) => {
      let eventHandler;
      let callBack = events[event];
      if (event === "onClick" || event === "onMouseEnter") {
        eventHandler = (e) => {
          callBack();
        };
        events[event] = eventHandler;
      }
    });
  }, [JSON.stringify(events)]);

  //setting mouse enter and leave event for the components children
  const childrens = React.Children.map(children, (child) => {
    return React.cloneElement(child, {
      ...events,
      ref: childRef,
    });
  });

  //initialize the position of child component at the first render
  useEffect(() => {
    setOffset(() => {
      return {
        x: childRef.current.offsetLeft,
        y: childRef.current.offsetTop,
      };
    });
  }, []);

  return { childrens, offset };
};

export default useAxis;

按钮(实际上是元素)组件:

//basic styles for icons
const iconsStyles = "w-[24px] h-[24px] transition duration-300 cursor-pointer";

    const NavBar = () => {
      const [showOptions, setShowOptions] = useState(false);
      return (
          <Options
            setShowOptions={setShowOptions}
            showOptions={showOptions}
          >
            //onClick will be applied to this div only
            <div
            >
              <TooltipContainer tooltipText="options">
                <div>
                  <DotsHorizontalIcon
                    className={`${iconsStyles} ${
                      showOptions
                        ? "text-[#fafafa]"
                        : "text-[#828282] hover:text-[#fafafa]"
                    }`}
                  />
                </div>
              </TooltipContainer>
            </span>
          </Options>
       //there are some other items that theres no need to be here
      );
    };
    
    export default NavBar;

您可以在此 CodeSandbox link 中查看我的代码和您需要查看的应用程序部分。 那么我应该怎么做才能避免在第一次单击打开弹出按钮将事件添加到document 后立即执行事件处理程序(用于外部单击目的)?

【问题讨论】:

    标签: javascript reactjs react-hooks event-handling dom-events


    【解决方案1】:

    我相信 on click 事件会冒泡,而状态已经切换,并且 ref 已初始化,因此在某个时候到达文档,其侦听器现在已注册然后调用。要停止这种行为,您需要在 useAxis 中调用 e.stopPropagation() 以获取点击事件。但是注册的按钮监听器不是你所期望的.它将注册您传递给useAxis 挂钩的侦听器,而不是useAxis 本身中修改后的侦听器。为了避免这种情况,只需将useEffect 中的代码放在useAxis 中即可。如果您随后在事件中调用e.stopPropagation(),它应该可以工作。这是useAxis中的最终部分代码:

      // now correctly updates events in time before cloning the children
      Object.keys(events).forEach((event) => {
        let eventHandler;
        let callBack = events[event];
        if (event === "onClick" || event === "onMouseEnter") {
          eventHandler = (e) => {
            // stop the propagation of the event
            e.stopPropagation();
            callBack();
          };
          events[event] = eventHandler;
          console.log("inner events", events);
        }
      });
    
      //setting updated mouse enter and leave event for the components children
      const childrens = React.Children.map(children, (child) => {
        console.log(events);
        return React.cloneElement(child, {
          ...events,
          ref: childRef
        });
      });
    

    【讨论】:

      【解决方案2】:

      问题

      您将在 Options.jsx 中返回以下 JSX。其中childrens 包含打开模式的按钮。如果您查看您的 JSX,此按钮将不会呈现为带有 boxRefdiv 的子元素,这就是为什么 useOutSideClick 中的 if (ref.current &amp;&amp; !ref.current.contains(event.target)) 将始终通过,因此它会立即关闭模式。

      return (
          <>
            {showOptions && (
              <div
                ref={boxRef}
                className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
                style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
              ></div>
            )}
            {childrens}
          </>
        );
      

      解决方案

      一个快速的解决方案是将上面的return 更改为下面的。请注意ref 被添加到外部div,因此用于打开模式的按钮呈现为其子级。

       return (
          <div ref={boxRef}>
            {showOptions && (
              <div
                className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
                style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
              ></div>
            )}
            {childrens}
          </div>
        );
      

      工作代码沙盒here

      【讨论】:

        【解决方案3】:

        同样,您可以在 addEventListeners {capture: true} 中将 capture 设置为等于 true

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-22
          • 2015-04-28
          • 1970-01-01
          • 2016-09-30
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多