Search code examples
javascriptcssreactjsreact-hooksjsx

Call custom hook only on the active component in the DOM


My goal is to detect a click outside of the components using this custom hook:

const useOutsideClick = (callback) => {
  const reference = useRef();

  useEffect(() => {
    const handleClick = (event) => {
      if (reference.current && !reference.current.contains(event.target)) {
        console.log(reference.current);
        console.log(event.target);

        callback();
      }
    };

    document.addEventListener("click", handleClick);

    return () => {
      document.removeEventListener("click", handleClick);
    };
  }, [reference]);

  return reference;
};

I have a <HeaderSearch /> custom component which looks like this:

const HeaderSearch = ({ screenWidth }) => {
   
  function checkInput(event) {
    if (event.keyCode !== 13) {
      setSearch(event.target.value);
    }
  }
  const mobileRef = useOutsideClick(test);
  const ref = useOutsideClick(test);

  if (location.pathname !== "/") {
    if (screenWidth == "mobile") {
      return (
        <div id="search-header" className="search-header mob" ref={mobileRef}>
          <FontAwesomeIcon
            className="search-icon"
            id="search-icon"
            icon={faSearch}
            onClick={() => toggleSearchInput()}
          />
          <input
            id="search-input"
            className={searchActive ? "search-input active" : "search-input"}
            type="text"
            value={search}
            onKeyDown={(e) => checkInput(e)}
            onChange={(e) => setSearch(e.target.value)}
            // ref={searchInputRef}
            // onClick={() => searchReqClientSide()}
            placeholder="Search..."
          />
          <button></button>
        </div>
      );
    } else {
      return (
        <div id="search-header" className="search-header desk" ref={ref}>
          <FontAwesomeIcon
            className="search-icon"
            id="search-icon"
            icon={faSearch}
            onClick={() => toggleSearchInput()}
          />
          <input
            id="search-input"
            className={searchActive ? "search-input active" : "search-input"}
            type="text"
            value={search}
            onKeyDown={(e) => checkInput(e)}
            onChange={(e) => setSearch(e.target.value)}
            // ref={searchInputRef}
            // onClick={() => searchReqClientSide()}
            placeholder="Search..."
          />
          <button></button>
        </div>
      );
    }
  } else {
    return null;
  }
};

The HeaderSearch component is displayed conditionally by CSS classes. If parent header element has class "active" change display of header without active to display "none".

However when I click outside of the component, the custom hook is still being called for both of the HeaderSearch components. See screenshot below:

enter image description here

How can I make it that only the <HeaderSearch/> which is active calls the useOutsideClick hook instead of both firing every click?

Here is the test callback, it's a simple log:

function test() {
  console.log("clicked outside");
}

Solution

  • Refactor the logic such that only one useOutsideClick hook is necessary. Since the header content, i.e. the children, appear to be same for both mobile and desk view, you can refactor to replace the wrapping div elements with a single div that conditionally applies the correct CSS className prop, and any other layout/UI differences between mobile and desk views.

    Example Implementation:

    const HeaderSearch = ({ screenWidth }) => {
      const ref = useOutsideClick(test);
      ...
    
      function checkInput(event) {
        if (event.keyCode !== 13) {
          setSearch(event.target.value);
        }
      }
    
      if (location.pathname !== "/") {
        return (
          <div
            ref={ref}
            id="search-header"
            className={[
              "search-header",
              screenWidth == "mobile" ? "mob" : "desk"
            ].join(" ")}
          >
            <FontAwesomeIcon
              className="search-icon"
              id="search-icon"
              icon={faSearch}
              onClick={() => toggleSearchInput()}
            />
            <input
              id="search-input"
              className={["search-input", searchActive && "active"]
                .filter(Boolean)
                .join(" ")
              }
              type="text"
              value={search}
              onKeyDown={(e) => checkInput(e)}
              onChange={(e) => setSearch(e.target.value)}
              // ref={searchInputRef}
              // onClick={() => searchReqClientSide()}
              placeholder="Search..."
            />
            <button></button>
          </div>
        );
      } else {
        return null;
      }
    };