简体   繁体   中英

React - How do I detect if a child component is clicked

I have a test site HERE

Please do:

  • Visit the site and click on the hamburger icon in the top right. The side nav should open.
  • Perform the same action again and you will see the problem I am currently having.

The side nav does not close properly because I have two conflicting functions operating.

The first conflicting function is an onClick toggle function within the actual hamburger component which toggles an associated context state.

The second conflicting function is used by the side nav panel component. It is bound to a hook which uses its ref to check whether the user has clicked inside or outside of the component.

These functions work up until the user clicks on the hamburger menu whilst the side nav is open. The hamburger is technically outside of the side nav component but overlayed on top of it, so the click does not register as outside. This results in both functions firing one after the other. The "click outside" hook fires and sets the side nav context to false, closing it. The hamburger toggle function then fires, finds the context set to false and changes it back to true. Thus instantly closing and then reopening the side nav.

The solution I have attempted looks like the below. I tried to assign a ref within the hamburger child component and pass it to the side nav parent. I wanted to try and compare the callback returned from useClickedOutside to the toggleRef given to the parent but the hamburger component and then if they match then do nothing. This, I was hoping, would knock one of the functions out of action for that interaction. Not entirely sure this is the best way to attempt to achieve something like this.

Perhaps I could get the side nav bounding box and then check if the click coordinates land within it?

//SideNav.jsx

const SideToggle = ({ sideNavToggle, sideIsOpen, setToggleRef }) => {
  useEffect(() => {
    const toggleRef = React.createRef();
    setToggleRef(toggleRef);
  }, []);

  return (
    <Toggle onClick={sideNavToggle}>
      <Bars>
        <Bar sideIsOpen={sideIsOpen} />
        <Bar sideIsOpen={sideIsOpen} />
        <Bar sideIsOpen={sideIsOpen} />
      </Bars>
    </Toggle>
  );
};

export default function SideNav() {
  const [toggleRef, setToggleRef] = useState(null);

  const { sideIsOpen, sideNavToggle, setSideIsOpen } = useContext(
    NavigationContext
  );
  const handlers = useSwipeable({
    onSwipedRight: () => {
      sideNavToggle();
    },
    preventDefaultTouchmoveEvent: true,
    trackMouse: true,
  });
  const sideRef = React.createRef();

  useClickedOutside(sideRef, (callback) => {
    if (callback) {
      if (callback === toggleRef) {
        return null;
      } else setSideIsOpen(false);
    }
  });

  return (
    <>
      <SideToggle
        sideIsOpen={sideIsOpen}
        sideNavToggle={sideNavToggle}
        setToggleRef={setToggleRef}
      />
      <div ref={sideRef}>
        <Side sideIsOpen={sideIsOpen} {...handlers}>
          <Section>
            <Container>
              <Col xs={12}>{/* <Menu /> */}</Col>
            </Container>
          </Section>
        </Side>
      </div>
    </>
  );
}



The useClickedOutside hook used above.

//use-clicked-outside.js

export default function useClickedOutside(ref, callback) {
  useEffect(() => {
    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        callback(event.target);
      } else return null;
    }

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

EDIT

Managed to fix part of the problem by moving the toggle component inside of the side nav component. It now has its own problem in that the hamburger component completely rerenders when the side nav props change. Trying to figure out how to stop the hamburger rerendering in a hooks scenario such as this. I've been reading that React.memo may be the answer but unsure how to implement it as of yet.

//SideNav.jsx

export default function SideNav() {
  const { sideIsOpen, sideNavClose, sideNavOpen } = useContext(
    NavigationContext
  );

  const handlers = useSwipeable({
    onSwipedRight: () => {
      sideNavClose();
    },
    preventDefaultTouchmoveEvent: true,
    trackMouse: true,
  });

  const sideRef = React.createRef();

  useClickedOutside(sideRef, (callback) => {
    if (callback) {
      sideNavClose();
    }
  });

  const SideToggle = () => {
    const handleClick = () => {
      if (!sideIsOpen) {
        sideNavOpen();
      }
    };

    return (
      <Toggle onClick={() => handleClick()}>
        <Bars>
          <Bar sideIsOpen={sideIsOpen} />
          <Bar sideIsOpen={sideIsOpen} />
          <Bar sideIsOpen={sideIsOpen} />
        </Bars>
      </Toggle>
    );
  };

  return (
    <>
      <SideToggle />
      <div ref={sideRef}>
        <Side sideIsOpen={sideIsOpen} {...handlers}>
          <Section>
            <Container>
              <Col xs={12}>{/* <Menu /> */}</Col>
            </Container>
          </Section>
        </Side>
      </div>
    </>
  );
}

I'd recommend using this package: https://github.com/airbnb/react-outside-click-handler . It handles some edge cases you mention, you can check their source code if you are interested in the details.

Then you need to keep the information about whether the sidebar is opened in the state and pass it down to the affected components somewhat like this (this way you can also change how the site looks in other places depending on the toolbar state, eg. disable scrollbar etc):

function Site() {
  const [isOpened, setToolbarState] = React.useState(false);

  return (
    <React.Fragment>
      <Toolbar isOpened={isOpened} setToolbarState={setToolbarState} />
      {isOpened ? (<div>draw some background if needed</div>) : null}
      <RestOfTheSite />
    </React.Fragment>
  );
}

function Toolbar(props) {
  const setIsClosed = React.useCallback(function () {
    props.setToolbarState(false);
  }, [props.setToolbarState]);

  const setIsOpened = React.useCallback(function () {
    props.setToolbarState(true);
  }, [props.setToolbarState]);

  return ( 
    <OutsideClickHandler onOutsideClick={setIsClosed}>
      <div className="toolbar">
        <button onClick={setIsOpened}>open</button>
        ...
        {props.isOpened ? (<div>render links, etc here</div>) : null}
      </div>
    </OutsideClickHandler>
  );
}

This way you won't necessary need to juggle refs and handlers around too much.

The way I answered this was to avoid detecting a click outside of the side nav altogether and instead detected if the click was outside of the main body of the site.

If true, then I call the navigation context and set side nav to false the same as I was trying in the side nav itself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM