Everybody wants to access web apps from their mobile phone and that’s why developers must create their React apps in a mobile-first fashion. There are multiple problems when comes to building apps for various devices such as many display resolutions, screen orientation, and touch events. Today, I’ll show you how to handle touch events in like a pro without any NPM libraries. Obviously, libraries can make your lives easier but did you know it only takes 50 lines of code to create your own component to handle mobile swipe events? I will teach you how to do so. React As always, my articles are based on the problems I was solving in real life. Recently I decided to migrate my version of the 2048 Game to Next.js and React 18 and this inspired me to rethink some parts of the initial design. To eliminate confusion, I will only focus on handling touch events in React and show you the best practices when comes to creating React apps that support mobile devices. open-source If you are intrigued by how to build the 2048 game from scratch, you should consider enrolling to my React course on Udemy. Link below. I will walk you through the entire process of creating the 2048 Game with animations. In the end, learning should bring us joy and fulfillment and nothing is more satisfying than making a complete game from scratch. Quick intro In early 2020, I released the very first version of my 2048 game. I wrote a short article describing my decision process and why I designed my code in a different way than other open-source versions. Immediately my article went viral on Reddit and FreeCodeCamp since it completely changed the perspective on this problem. My implementation was different than others because it supported mobile devices and had awesome animations. Other open source implementations of 2048 in React were janky and incomplete. Despite my efforts, the initial version wasn’t perfect and that’s why I decided to refresh it. I updated it to React 18, changed the build process by migrating it to Next.js, and rewrote styles from LESS to classic CSS. Still, I wasn’t happy about the fact I used a lot of NPM libraries - especially libraries responsible for dealing with mobile touch events. I used a library called and it was perfect to support multi-gesture touch events in JavaScript. Unfortunately, doesn’t support React and it forced me to create my own React wrapper for this library. But I found yet another problem - the NPM library hadn’t been updated for the last 8 years and it puzzled me. is downloaded more than 1.4 million times weekly but nobody is maintaining it. hammerjs hammerjs hammerjs Technically, my project wasn’t critical so I still could take advantage of this library. I always prioritize high quality of code but usage of unmaintained library cannot be considered as good practice. That’s why I created my component from scratch. Custom React Component - MobileSwiper I decided to call this component and declared two props as its arguments – and : MobileSwiper children onSwipe export default function MobileSwiper({ children, onSwipe }) { return null } Let me briefly explain them: allow me to inject any HTML elements inside this component. children is a callback that will be triggered each time user swipes within a given area. In my case, I want to enable mobile swipes on the game board only. All other parts of app should be scrollable. onSwipe To detect the swipeable area I created a reference to the DOM element. Basically, only this element will allow custom touch events. I did it by using the hook, and using it to reference a element: useRef div import { useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { const wrapperRef = useRef(null) return <div ref={wrapperRef}>{children}</div> } Once I declared the area I was ready to focus on handling swipes. The easiest way to detect them is by comparing the starting and ending positions of the user's finger. This means I needed to store the initial position of the user's finger in state ( and coordinates) and compare it with the last known finger position (before the user lifts the finger off their screen): x y import { useState, useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { const wrapperRef = useRef(null) const [startX, setStartX] = useState(0) const [startY, setStartY] = useState(0) return <div ref={wrapperRef}>{children}</div> } Then I was ready to implement finger tracking, I created two callbacks using standard JavaScript touch events: used to set the starting position of the user's finger. handleTouchStart handling the final position of the user's finger and calculating the shift (delta) from the starting point. handleTouchEnd Let's look into the event handler first: handleTouchStart import { useCallback, useState, useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { const wrapperRef = useRef(null) const [startX, setStartX] = useState(0) const [startY, setStartY] = useState(0) const handleTouchStart = useCallback((e) => { if (!wrapperRef.current.contains(e.target)) { return } e.preventDefault() setStartX(e.touches[0].clientX) setStartY(e.touches[0].clientY) }, []) return <div ref={wrapperRef}>{children}</div> } Let me explain it: I wrapped this helper into the hook to improve the performance (caching). useCallback The statement checks if user is swiping within a declared area. If they do it outside of it, the event will not be triggered and user will simply scroll the application. if disables the default scrolling event. e.preventDefault() The last two lines store and coordinates in state. x y Now let's focus the event handler: handleTouchEnd import { useCallback, useState, useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { const wrapperRef = useRef(null) const [startX, setStartX] = useState(0) const [startY, setStartY] = useState(0) const handleTouchStart = useCallback((e) => { if (!wrapperRef.current.contains(e.target)) { return } e.preventDefault() setStartX(e.touches[0].clientX) setStartY(e.touches[0].clientY) }, []) const handleTouchEnd = useCallback((e) => { if (!wrapperRef.current.contains(e.target)) { return } e.preventDefault() const endX = e.changedTouches[0].clientX const endY = e.changedTouches[0].clientY const deltaX = endX - startX const deltaY = endY - startY onSwipe({ deltaX, deltaY }) }, [startX, startY, onSwipe]) return <div ref={wrapperRef}>{children}</div> } As you can see, I’m taking the final and coordinates of user's finger and deduct the starting and coordinates from the final ones. Thanks to that I am able to calculate horizontal and vertical shift of the finger (aka deltas). x y x y Then I passed calculated deltas onto the callback which will be coming from other components to promote reusability. PS. If you remember, I declared the callback in the component's props. onSwipe onSwipe In the end, I needed to take advantage of created event callbacks and hook them into the event listener: import { useCallback, useEffect, useState, useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { const wrapperRef = useRef(null) const [startX, setStartX] = useState(0) const [startY, setStartY] = useState(0) const handleTouchStart = useCallback((e) => { if (!wrapperRef.current.contains(e.target)) { return } e.preventDefault() setStartX(e.touches[0].clientX) setStartY(e.touches[0].clientY) }, []) const handleTouchEnd = useCallback( (e) => { if (!wrapperRef.current.contains(e.target)) { return } e.preventDefault() const endX = e.changedTouches[0].clientX const endY = e.changedTouches[0].clientY const deltaX = endX - startX const deltaY = endY - startY onSwipe({ deltaX, deltaY }) }, [startX, startY, onSwipe]) useEffect(() => { window.addEventListener("touchstart", handleTouchStart) window.addEventListener("touchend", handleTouchEnd) return () => { window.removeEventListener("touchstart", handleTouchStart) window.removeEventListener("touchend", handleTouchEnd) } }, [handleTouchStart, handleTouchEnd]) return <div ref={wrapperRef}>{children}</div> } Let’s use it The component is ready now and I can use it with my other components. As I mentioned at the beginning, I created this component to handle mobile swipes on the game board of my 2048 Game, and that’s where this example code comes from. MobileSwiper If you want to use it in your project feel free to copy & paste the function and hook it as I did it on the game board: handleSwipe import { useCallback, useContext, useEffect, useRef } from "react" import { Tile as TileModel } from "@/models/tile" import styles from "@/styles/board.module.css" import Tile from "./tile" import { GameContext } from "@/context/game-context" import MobileSwiper from "./mobile-swiper" export default function Board() { const { getTiles, moveTiles, startGame } = useContext(GameContext); // ... removed irrelevant parts ... const handleSwipe = useCallback(({ deltaX, deltaY }) => { if (Math.abs(deltaX) > Math.abs(deltaY)) { if (deltaX > 0) { moveTiles("move_right") } else { moveTiles("move_left") } } else { if (deltaY > 0) { moveTiles("move_down") } else { moveTiles("move_up") } } }, [moveTiles]) // ... removed irrelevant parts ... return ( <MobileSwiper onSwipe={handleSwipe}> <div className={styles.board}> <div className={styles.tiles}>{renderTiles()}</div> <div className={styles.grid}>{renderGrid()}</div> </div> </MobileSwiper> ) } Let's focus on the handler first. As you can see, I‘m comparing if is greater than to estimate if the user swiped horizontally (left/right) or vertically (top/bottom). handleSwipe deltaX deltaY If it was a horizontal swipe, then: negative means they swiped to the left. deltaX positive means they swiped to the right. deltaX If it was a vertical swipe, then: negative means they swiped up. deltaY positive means they swiped down. deltaY Now, let's move on to the statement of the Board component. As you can see, here’s where my custom MobileSwiper component comes into play. I am passing the helper to the property, and wrapping the HTML code of the game board to enable swiping on it. return handleSwipe onSwipe It works but unfortunately, the result isn't perfect - scrolling events are mixed up with swipes: This is happening because modern browsers use passive event listeners to improve the scrolling experience on mobile devices. it means that the I added that event handlers never take effect. preventDefault To fix scrolling behavior, I disabled passive listeners on the component by setting flag to : MobileSwiper passive false import { useCallback, useEffect, useState, useRef } from "react" export default function MobileSwiper({ children, onSwipe }) { // ... removed to improve visibility ... useEffect(() => { window.addEventListener("touchstart", handleTouchStart, { passive: false }) window.addEventListener("touchend", handleTouchEnd, { passive: false }) // ... removed to improve visibility ... }, [handleTouchStart, handleTouchEnd]) // ... removed to improve visibility ... } Now the scrolling behavior is gone and my 2048 Game works like a charm: Conclusion I am not against using libraries because they can speed up your work but sometimes you can build your custom solution in 50 lines of code just like I showed you today. That’s why i believe that you don’t need any library to handle mobile events in React. Wanna learn more tricks like this one? If you wish to learn more React tricks like this one, or simply you want to say ‘thank you’, consider joining my course on Udemy where I will teach you how to . You will not only learn clever tricks like that one but also find solutions to the most common mistakes that React developers make. create the 2048 game in React (special discount for HackerNoon readers) 🧑🎓 Join React 18 course on Udemy Believe it or not, this is the most up-to-date React 18 and Next.js course on Udemy. I released in January 2024 and I’ll keep updating it.