0

I created a Dropdown that when I click outside of it the dropdown disappears. I used a click event listener to determine if I clicked outside the dropdown.

After a few clicks, the page slows down and crashes. Perhaps the state is being rendered in a loop or too many events are being fired at once?

How do I fix this? Also, is there a more React way to determine if I clicked outside an element? (Instead of using a document.body event listener)

Here is the codepen:

const items = [
    {
        value: 'User1'
    },
    {
        value: 'User2'
    },
    {
        value: 'User3'
    },
    {
        value: 'User4'
    },
    {
        value: 'User5'
    }
];
    
class Dropdown extends React.Component {
  state = {
    isActive: false,
  }

  render() {
    const { isActive } = this.state;
    document.addEventListener('click', (evt) => {
        if (evt.target.closest('#dropdownContent')) {
          //console.warn('clicked inside target do nothing');
          return;
        }

        if (evt.target.closest('#dropdownHeader')) {
          //console.warn('clicked the header toggle');
          this.setState({isActive: !isActive});
        }

        //console.warn('clicked outside target');
        if (isActive) {
          this.setState({isActive: false});
        }
      });
    
      return (
          <div id="container">
              <div id="dropdownHeader">select option</div>
              {isActive && (
                <div id="dropdownContent">
                  {items.map((item) => (
                    <div id="item" key={item.value}>
                      {item.value}
                    </div>
                  ))}
                </div>
              )}
          </div>
      );
  };
}
  ReactDOM.render(
    <Dropdown items={items} />,
    document.getElementById('root')
  );
#container {
    position: relative;
    height: 250px;
    border: 1px solid black;
}

#dropdownHeader {
    width: 100%;
    max-width: 12em;
    padding: 0.2em 0 0.2em 0.2em;
    margin: 1em;
    cursor: pointer;
    box-shadow: 0 1px 4px 3px rgba(34, 36, 38, 0.15);
}

#dropdownContent {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 3em;
    width: 100%;
    max-width: 12em;
    margin-left: 1em;
    box-shadow: 0 1px 4px 0 rgba(34, 36, 38, 0.15);
    padding: 0.2em;
}

#item {
    font-size: 12px;
    font-weight: 500;
    padding: 0.75em 1em 0.75em 2em;
    cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

<div id="root">
    <!-- This element's contents will be replaced with your component. -->
</div>

5
  • Um, you don't need to link to your code. You can provide a runnable example free, online inside your question. Commented Aug 24, 2019 at 1:41
  • @zixuan is there a way to make reactjs code runnable inside your question? Commented Aug 24, 2019 at 1:42
  • oh never did it before. let me try again Commented Aug 24, 2019 at 1:44
  • sorry but you can convert it to standard JS @ngood97 Commented Aug 24, 2019 at 1:48
  • 1
    Made it runnable inside question -- thanks for quick feedback Commented Aug 24, 2019 at 1:49

1 Answer 1

1

There's a pretty simple explanation for what you're experiencing. :)

The way I was able to figure it out was the number of warnings that were showing up in the terminal every time I clicked somewhere was getting higher and higher, especially when the state changed.

The answer though is that since you were adding the event listener code in the render function, every time the code re-rendered it would add more and more event listeners slowing down your code.

Basically the solution is that you should move the adding of event listeners to componentDidMount so it's only run once.

Updated working javascript:

    const items = [
        {
            value: 'User1'
        },
        {
            value: 'User2'
        },
        {
            value: 'User3'
        },
        {
            value: 'User4'
        },
        {
            value: 'User5'
        }
    ];

class Dropdown extends React.Component {
  state = {
    isActive: false,
  }

  // added component did mount here
  componentDidMount(){
    const { isActive } = this.state;
    document.addEventListener('click', (evt) => {
        if (evt.target.closest('#dropdownContent')) {
          console.warn('clicked inside target do nothing');
          return;
        }

        if (evt.target.closest('#dropdownHeader')) {
          console.warn('clicked the header toggle');
          this.setState({isActive: !isActive});
        }

        console.warn('clicked outside target');
        if (isActive) {
          this.setState({isActive: false});
        }
      });
  }

  render() {
    const { isActive } = this.state;
    //removed event listener here
      return (
          <div id="container">
              <div id="dropdownHeader">select option</div>
              {isActive && (
                <div id="dropdownContent">
                  {items.map((item) => (
                    <div id="item" key={item.value}>
                      {item.value}
                    </div>
                  ))}
                </div>
              )}
          </div>
      );
  };
}
  ReactDOM.render(
    <Dropdown items={items} />,
    document.getElementById('root')
  );
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.