Event handler get's called before handler is set

Viewed 277

I have this reusable component in my react app.

export const OutsideWrapper = ({ children, onOutside, className }) => {
  const wrapperRef = useRef(null);
  const [style, setStyles] = useState({
    opacity: 1
  });
  useEffect(() => {
    console.log("1. component was mounted");
    const i = e => {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
        console.log("3. outside click was trigerred");
        e.preventDefault();
        setStyles({ opacity: 0 });
        setTimeout(() => {
          onOutside();
        }, 100);
      }
    };
    window.addEventListener("click", i, true);
    console.log('2. listener was added');
    return () => {
      console.log("4. listerner was removed");
      window.removeEventListener("click", i, true);
    };
  }, [onOutside]);
  return (
    <div
      ref={wrapperRef}
      style={style}
      className={`outside-wrapper ${className}`}
    >
      {children}
    </div>
  );
};

When this OutsideWrapper component get's rendered, it should add event listener to the document, then listen to an event, call onOutside function and then unmount. (onOutside unmounts the component). After that listener gets removed.

But when component get's rendered, it immediately calls onOutside and unmounts.

Here's the part of parent component:

const [down, setDown] = useState(false);
return (
    <input onFocus={()=>setDown(true)}/>
       {down && (
          <OutsideWrapper
             onOutside={() => setDown(false)}
             className="input-wrapper"
           >
            <DropDownList
              items={dropDownItems}
              term={data.location}
              onChoose={onChoose}
             />
          </OutsideWrapper>
       )}
    )
1 Answers

The window.addEventListener call inside useEffect is being called when React's rendering the component, which happens on focus. The event that causes focus isn't the click though, it's a mousedown. When the next mouseup happens, a click event is also generated which is caught. Notice that if you tab into the input to focus it doesn't cause the bug.

There are a couple approaches to fix this, but my recommendation is to ignore click events that happen on the input itself.

Here's an example: I've added a ref to the <input>, passed it into OutsideWrapper, and added a check like you have for wrapperRef for the new ref.

function Test() {
  const [down, setDown] = React.useState(false);
  const focusRef = React.useRef();
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>
        <input ref={focusRef} onFocus={() => setDown(true)} />
        {down && (
          <OutsideWrapper
            onOutside={() => setDown(false)}
            focusedRef={focusRef}
            className="input-wrapper"
          >
            children
          </OutsideWrapper>
        )}
      </h2>
    </div>
  );
}

const OutsideWrapper = ({ children, onOutside, className, focusedRef }) => {
  const wrapperRef = React.useRef(null);
  const [style, setStyles] = React.useState({
    opacity: 1
  });
  React.useEffect(() => {
    console.log("1. component was mounted");
    const i = e => {
      console.log(e.target, wrapperRef.current);
      if (
        wrapperRef.current &&
        !wrapperRef.current.contains(e.target) &&
        focusedRef.current !== e.target
      ) {
        console.log("3. outside click was trigerred");
        e.preventDefault();
        setStyles({ opacity: 0 });
        setTimeout(() => {
          onOutside();
        }, 100);
      }
    };
    window.addEventListener("click", i, true);
    console.log("2. listener was added");
    return () => {
      console.log("4. listerner was removed");
      window.removeEventListener("click", i, true);
    };
  }, [onOutside, focusedRef]);
  return (
    <div
      ref={wrapperRef}
      style={style}
      className={`outside-wrapper ${className}`}
    >
      {children}
    </div>
  );
};
Related