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 theLink
component from the gatsby library. Could these hooks be interfering with the functionality of theLink
?
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 thetouchend
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
ortouchend
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