1

After searching the subject "Detect outside click React Hooks component", I can't figure out the solution for improvement performance my current application.

  1. Context: I have multiple React components:

    • App: root component, has itemSelecting state to detect current item selecting (FirstComponent or SecondComponent, two different components). It has an mousedown/mouseup event listener to detect outside click FirstComponent or SecondComponent
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

import './styles.css';

import FirstComponent from './components/FirstComponent';
import SecondComponent from './components/SecondComponent';
import DropDownComponent from './components/DropdownComponent';

function App() {
  const [itemSelecting, setItemSelecting] = useState(0);

  function handleClickOutside(event) {
    // FIXME: how to detect outside click 4 components
    // console.log(event.target);
    if (itemSelecting !== -1) setItemSelecting(0);
    console.log('click first/second component');
  }

  useEffect(() => {
    if (itemSelecting !== -1) {
      document.addEventListener('mousedown', handleClickOutside);
    } else {
      document.removeEventListener('mousedown', handleClickOutside);
    }
  }, []);

  const handleClick = value => {
    if (value === itemSelecting) setItemSelecting(0);
    else setItemSelecting(value);
  };

  return (
    <React.Fragment>
      <div className="App">
        <FirstComponent
          label="01"
          selected={itemSelecting === 1}
          handleClick={() => handleClick(1)}
        />
        <SecondComponent
          label="02"
          selected={itemSelecting === 2}
          handleClick={() => handleClick(2)}
        />
        <FirstComponent
          label="03"
          selected={itemSelecting === 3}
          handleClick={() => handleClick(3)}
        />
        <SecondComponent
          label="04"
          selected={itemSelecting === 4}
          handleClick={() => handleClick(4)}
        />
      </div>
      <hr />
      <DropDownComponent />
    </React.Fragment>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
  • FirstComponent: the square component has selected props passed by App. It can be selected (inside click) / unselected (outside click or click again when its selected)
import React from 'react';

import cn from 'classnames';

import styles from '../styles.module.css';

const FirstComponent = ({ label, selected, handleClick }) => {
  const selectedClassName = selected ? styles.selected : '';

  return (
    <div
      className={cn([styles.component, styles.first, selectedClassName])}
      onClick={handleClick}>
      <span>{label}</span>
    </div>
  );
};

export default FirstComponent;
  • SecondComponent: the circle component has selected props passed by App. It can be selected (inside click) / unselected (outside click or click again when its selected)
import React from 'react';

import cn from 'classnames';

import styles from '../styles.module.css';

const SecondComponent = ({ label, selected, handleClick }) => {
  const selectedClassName = selected ? styles.selected : '';

  return (
    <div
      className={cn([styles.component, styles.second, selectedClassName])}
      onClick={handleClick}>
      <span>{label}</span>
    </div>
  );
};

export default SecondComponent;
  • DropdownComponent: the dropdown component can be expanded (inside click) / collapsed (outside click). It has another mousedown/mouseup event listener to detect outside click DropDownComponent.
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import styles from '../styles.module.css';

const DropDownComponent = () => {
  const nodeRef = useRef(null);
  const listRef = useRef(null);

  const [isOpen, setIsOpen] = useState(false);

  const handleClick = event => {
    if (nodeRef && nodeRef.current.contains(event.target)) {
      // inside click
      if (
        listRef &&
        listRef.current &&
        listRef.current.contains(event.target)
      ) {
        setTimeout(() => {
          setIsOpen(false);
        }, 500);
      }
      return;
    }
    setIsOpen(false);
    console.log('outside click dropdown');
  };

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

  return (
    <div ref={nodeRef} className={styles.dropdown}>
      <button className={styles.dropbtn} onClick={() => setIsOpen(!isOpen)}>
        Dropdown
      </button>

      {isOpen && (
        <div ref={listRef} className={styles.dropdownContent}>
          <a>Link 1</a>
          <a>Link 2</a>
          <a>Link 3</a>
        </div>
      )}
    </div>
  );
};

DropDownComponent.defaultProps = {
  classNameType: null,
  isOpen: false,
  handleIsOpen: () => {},
  title: null,
};

DropDownComponent.propTypes = {
  classNameType: PropTypes.string,
  isOpen: PropTypes.bool,
  handleIsOpen: PropTypes.func,
  title: PropTypes.string,
};

export default DropDownComponent;
  1. Problem: In the same time, it have some outside click event listener redundant. Example: when clicking on one of FirstComponent / SecondComponent, it appears:
outside click dropdown 
click first/second component
  1. Target: how can I detect once at a time the outside click event listener on multiple components? I want to optimize: when clicking on one of FirstComponent / SecondComponent, only outside click DropdownComponent is invoked.

Note: In fact, I would like to implement ref (useRef) inside each components (FirstComponent / SecondComponent) to resolve it but I don't know how (FIXME in App.js)

The source code demo is on CodeSandbox.

Thanks in advance.

nhbduy
  • 13
  • 4

1 Answers1

0

I think your problem is that the selected component deselects first when you click on them. That is because the onClick event fires when the mouse button released, while the outside click fires at the moment of mouse down. This can be fixed with replacing mousedown with mouseup in the addEventListener for outside click.

The other potential problem could be, that you do not remove the listener from the window. You should clear up all your subscription in a return function of the useEffect hook. For this it is better to move the event handler declaration inside the useEffect. And because the outside click event handler uses the itemSelecting value, you have to add it as a dependency to the useEffect.

useEffect(() => {
  const handleClickOutside = (event) => {
    if (itemSelecting !== -1) setItemSelecting(0);
    console.log('click first/second component');
  }
  document.addEventListener('mouseup', handleClickOutside);
  return () => document.removeEventListener('mouseup', handleClickOutside);
}, [itemSelecting]);

It is optional, but I would remove the itemSelecting dependency from the handleClickOutside handler. So The useEffect only called once. This way it could be more simpler.

useEffect(() => {
  const handleClickOutside = (event) => {
    setItemSelecting(0);
    console.log('click first/second component');
  }
  document.addEventListener('mouseup', handleClickOutside);
  return () => document.removeEventListener('mouseup', handleClickOutside);
}, []);

I hope it helps. https://codesandbox.io/s/detect-multiple-outside-click-on-react-components-1hjg0

Peter Ambruzs
  • 7,763
  • 3
  • 30
  • 36