June 10, 2019

Improving Touch Events upon an Infinite Scrolling Component

In my recent blog post on Using React Hooks to set up Infinite Scroll, I created a working version of infinite scroll that works in both desktop and touch screen environments. However, I came across a problem I did not anticipate when I put it into production, some of the links on my page stopped working. I tried debugging my CSS, to no avail, only to realize the solution involved updating my touchend event handlers with a very simple fix.

Solution: Debugging CSS ??

The structure of my components nests some links within block level elements. To achieve infinite-scrolling, I map over a list of businesses as follows:

<li key={govId} index={idx + 1}>
  <h2>
    <Link 
      to={ `/businesses/${businessName}` }      state={{ 
        prevPath: typeof window !== `undefined` ? window.location.pathname : ''
      }}
    >
      {businessName}
    </Link>
  </h2>
  <p>
    <a href={`tel:${businessPhone}`}>{businessPhone}</a>  </p>
</li>

From past experience, I immediately thought the solution would be to either adjust the z-index of the nested link or to set pointer-events on the parent elements. I tried both:

    li, li>h2, p { 
        pointer-events: auto;
        z-index: 1;
    }
    li>h2>a, p>a {
        z-index: 3;
    }

Neither solution solved the problem nor were a problem to begin with, at least in my implementation.

Solution: Debugging touchend Event Handler !!

First, here is the initial state of my code for this component. I define handleScroll to add more items to the infinite scroll. Then, handleTouchEnd calls the scroll event handler to avoid double loading. Take particular note of e.preventDefault (perhaps this shouldn’t be called at all? We shall find out soon enough):

  const handleScroll = () => {
    if ( !hasMore ) return;
    if ( window && ( window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight ) ){
      loadMore() // function to add more items to the infinite scroll until no items left
    }
  }

  const handleTouchEnd = (e) => {
      e.preventDefault();      handleScroll();
  }
  
  useEffect(() => {
    window && window.addEventListener('touchend', handleTouchEnd)
    window && window.addEventListener('scroll', handleScroll)
    window && window.addEventListener('resize', handleScroll)
    return () => {
      window && window.removeEventListener('touchend', handleTouchEnd)
      window && window.removeEventListener('scroll', handleScroll)
      window && window.removeEventListener('resize', handleScroll)
    };
  }, [businesses, hasMore])

After realizing that updated my css would not resolve my issue, I wondered what in the world is causing my problem. In this particular component, I use two React hooks to manage state and handle events - useState and useEffect. I wondered,

Okay, so this is a Gatsby project, and I'm importing the Link component from the gatsby library. Could these hooks be interfering with the functionality of the Link?

This wasn’t the problem. I could see in the console that Link rendered a simple a tag, and other Link components were working on the page, such as those rendered by the Navigation component. The only links not working were within my infinite scrolling list component. Moreover, the links worked perfectly in a desktop environment. So I again wondered,

Why in the world does this work on desktop and not on mobile? No errors are being thrown. The href attribute is valid and working if I paste it in the browser. How again does the touchend event work?

This led me to investigating the order in which events are fired by touch screens.

click comes after touchend

According to MDN, the W3C standard calls for a typical order of events fired by touch screens, as follows:

  • touchstart
  • Zero or more touchmove events, depending on movement of the finger(s)
  • touchend
  • mousemove
  • mousedown
  • mouseup
  • click

Remember I asked you to take particular note that I was calling e.preventDefault() on the touchend event? Turns out that is the culprit. By cancelling the dispatching of further events on touchend, the click event for the link component was never being fired. As MDN tells us:

If the touchstart, touchmove or touchend event is canceled during an interaction, no mouse or click events will be fired.

The solution then must include not calling e.preventDefault, particularly when a link is the target of touchend.

So, the only change necessary involves adding a condition within my handleTouchEnd function, to check for a tags, or Element.tagName == "A", and only call e.preventDefault() if the target is not such a tag:

const handleTouchEnd = (e) => {
  if (e.target.tagName !== "A") {
    e.preventDefault(); 
    handleScroll();
  } else {
    console.log("this makes me a click event, most likely")
  }
}

Photo by Amy Humphries on Unsplash

Written by Wesley L. Handy who lives and works in Virginia Beach, VA, finding ways to build cool stuff in Gatsby and React. You should follow him on Twitter