Search code examples
javascriptreactjstouchtouchstart

How to stop JavaScript events when React component unmounts?


Background:
I have a web app built with React (v. 16.4.2 currently). It will only ever be used on a touch screen. It is composed of a ton of buttons to do things, and since it's all touch, I'm using touchstart/touchend to handle these actions.

Example: This is a basic example of how I'm using the events. You click a button, it sets this.state.exampleRedirect to true, which in turn, makes the component re-render and then go to the new page (using react-router-dom). This is all working fine.

<button
  type='button'
  onTouchStart={() => this.setState({ exampleRedirect: true })}
  className='o-button'>
  Open modal
</button>

Issue:
I originally used onClick to handle buttons but had issues because my users have fat fingers and not a lot of tech background, and when they'd touch a button, they'd drag their finger over the button and it wouldn't fire the click. OnTouchStart fixes this problem by firing the minute any touch happens (drag, swipe, tap, etc).

The issue is with onTouchStart. A user touches the button, it quickly changes the page (using the router) and re-renders the new page. The app is fast, so this is almost instantaneous, which means that when the new page loads, the user's finger is usually still on the screen, thus firing ANOTHER touch event on whatever they're touching. This is often another routing button, so it just fires through screens until they lift their finger.

I am working around this by putting a delay on enabling buttons on each page load.

// example component
import React, { Component } from 'react';

class ExampleComponent extends Component {
  state = { buttonsDisabled: true }

  // after 300ms, the buttons are set to enabled (prevents touch events 
  // from firing when the page first loads
  componentWillMount() {
    timeoutId = setTimeout(() => {
      this.setState({ buttonsDisabled: false });
    }, 300);
  }

  render() {
    return (
      // button in render method
      <button
        disabled={this.state.buttonsDisabled}
        type='button'
        onTouchStart={() => this.setState({ exampleRedirect: true })}
        className='o-button'>
        Open modal
      </button>
    );
  }

Is there a better way? Or a way to do what I'm doing, but globally so I don't have to add this jankity code in about 100 components?

Thanks!


Solution

  • Instead of using the onTouchStart event which is fired when a touch point is placed on the touch surface and using a timeout, which is a bit of a hack, you should make use of onTouchEnd since it will be fired when a touch point is removed from the touch surface, thereby ensuring that the mentioned case doesn't happen.

    // example component
    import React, { Component } from 'react';
    
    class ExampleComponent extends Component {
      state = { buttonsDisabled: true }
    
      // after 300ms, the buttons are set to enabled (prevents touch events 
      // from firing when the page first loads
      componentWillMount() {
        timeoutId = setTimeout(() => {
          this.setState({ buttonsDisabled: false });
        }, 300);
      }
    
      render() {
        return (
          // button in render method
          <button
            disabled={this.state.buttonsDisabled}
            type='button'
            onTouchEnd={() => this.setState({ exampleRedirect: true })}
            className='o-button'>
            Open modal
          </button>
        );
      }