skip to Main Content

I’m trying to write a simple useClickOutside custom hook, that given a callback and an element, calls the callback whenever the user clicks outside of that element.

It looks something like this:

function useClickOutside(handler) {
  const ref = useRef();

  useEffect(() => {
    const handleClick = (e) => {
      if(ref.current && !ref.current.contains(e.target)) {
        handler();
      }
    };

    document.addEventListener('click', handleClick);
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, [handler]);

  return ref;
}

And to use it in a component (example):

function Example(props) {
  const menuRef = useClickOutside(() => {
      alert('You just clicked outside the menu.');
  });
  // some other component logic
  return (
    <div>
      <Menu ref={menuRef} />
    </div>
  );
}

This works fine and all but there is a problem: every time the Example component gets rerendered the useEffect inside the useClickOutside runs again, which is unnecessary. This is because handler is passed as a dependency to the useEffect (as the linter recommends), and every time Example rerenders, the callback is treated as a "new function" because of the way referential equality works.

Luckily, react has a solution to this, all you have to do is wrap the handler in a useCallback.

const menuRef = useClickOutside(useCallback(() => {
    alert('You just clicked outside the menu.');
}, []));

Now, the useEffect inside the useClickOutside only runs when the Example component is mounted in this particular case.

However, to me this seems really anti-pattern. There is nothing stopping us from passing a regular function without useCallback and it will result in unncessary calls to the useEffect.

Is there a better, more elegant way of writing this useClickOutside hook that potentially eliminates the need for useCallback?

2

Answers


  1. Not sure but You can use the useCallback hook to memoize the loadTab function so that it remains the same between renders. This ensures that the same function reference is passed to each SwitchTabButton instance, and the state changes work as expected. Try this code:-

    import { useState, useCallback } from "react";
    
    export default function Tabs() {
      console.log("create tabs");
      const SwitchTabButton = buttonCreator();
      const [tab, setTab] = useState(null);
    
      const loadTab = useCallback(
        (content) => {
          setTab(content);
        },
        [setTab]
      );
    
      const tabsData = [
        {
          name: "Button 1",
          content: "Tab 1",
        },
        {
          name: "Button 2",
          content: "Tab 2",
        },
        {
          name: "Button 3",
          content: "Tab 3",
        },
      ];
    
      return (
        <>
          {tabsData.map((data, index) => (
            <SwitchTabButton
              key={index}
              loadTab={loadTab}
              tabName={data.name}
              tabContent={data.content}
            />
          ))}
          {tab}
        </>
      );
    }
    
    Login or Signup to reply.
  2. Did you try to move out alert function as a const?

    const alertFunc = () => {
        alert('You just clicked outside the menu.');
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search