1

There's a solution here... Several, actually - but none of them work for React 17.0.2. They all result in

Error: Rendered more hooks than during the previous render.

Even with fixes listed in the comments (Using useref() instead of useState, for instance).

So my question is - how can I have long click/press/tap in React 17.0.2 and newer?

My attempt at fixing it:

//https://stackoverflow.com/questions/48048957/react-long-press-event
import {useCallback, useRef, useState} from "react";

const useLongPress = (
    onLongPress,
    onClick,
    {shouldPreventDefault = true, delay = 300} = {}
) => {
    //const [longPressTriggered, setLongPressTriggered] = useState(false);
    const longPressTriggered = useRef(false);
    const timeout = useRef();
    const target = useRef();
    
    const start = useCallback(
        event => {
            if (shouldPreventDefault && event.target) {
                event.target.addEventListener("touchend", preventDefault, {
                    passive: false
                });
                target.current = event.target;
            }
            timeout.current = setTimeout(() => {
                onLongPress(event);
                //setLongPressTriggered(true);
                longPressTriggered.current = true;
            }, delay);
        },
        [onLongPress, delay, shouldPreventDefault]
    );
    
    const clear = useCallback(
        (event, shouldTriggerClick = true) => {
            timeout.current && clearTimeout(timeout.current);
            shouldTriggerClick && !longPressTriggered && onClick(event);
            //setLongPressTriggered(false);
            longPressTriggered.current = false;
            if (shouldPreventDefault && target.current) {
                target.current.removeEventListener("touchend", preventDefault);
            }
        },
        [shouldPreventDefault, onClick, longPressTriggered]
    );
    
    return {
        onMouseDown: e => start(e),
        onTouchStart: e => start(e),
        onMouseUp: e => clear(e),
        onMouseLeave: e => clear(e, false),
        onTouchEnd: e => clear(e)
    };
};

const isTouchEvent = event => {
    return "touches" in event;
};

const preventDefault = event => {
    if (!isTouchEvent(event)) return;
    
    if (event.touches.length < 2 && event.preventDefault) {
        event.preventDefault();
    }
};

export default useLongPress;

RandomItem.js:

import React, {useEffect, useState} from 'react';
import Item from "../components/Item";
import Loader from "../../shared/components/UI/Loader";
import {useAxiosGet} from "../../shared/hooks/HttpRequest";
import useLongPress from '../../shared/hooks/useLongPress';


function RandomItem() {
    let content = null;
    let item = useAxiosGet('collection');
    
    if (item.error === true) {
        content = <p>There was an error retrieving a random item.</p>
    }
    
    if (item.loading === true) {
        content = <Loader/>
    }
    
    if (item.data) {
        const onLongPress = useLongPress();
        return (
            content =
                <div>
                    <h1 className="text-6xl font-normal leading-normal mt-0 mb-2">{item.data.name}</h1>
                    <Item name={item.data.name} image={item.data.filename} description={item.data.description}/>
                </div>
        )
    }
    
    return (
        <div>
            {content}
        </div>
    );
}

export default RandomItem;
Jon
  • 305
  • 3
  • 20
  • 45

1 Answers1

2

The (unedited) useLongPress function should be used similar to the following example:

import React, { useState } from "react";
import "./styles.css";
import useLongPress from "./useLongPress";

export default function App() {
  const [longPressCount, setlongPressCount] = useState(0)
  const [clickCount, setClickCount] = useState(0)

  const onLongPress = () => {
    console.log('longpress is triggered');
    setlongPressCount(longPressCount + 1)
  };

  const onClick = () => {
    console.log('click is triggered')
    setClickCount(clickCount + 1)
  }

  const defaultOptions = {
    shouldPreventDefault: true,
    delay: 500,
  };
  const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);

  return (
    <div className="App">
      <button {...longPressEvent}>use  Loooong  Press</button>
      <span>Long press count: {longPressCount}</span>
      <span>Click count: {clickCount}</span>
    </div>
  );
}

Be sure to pass in the onLongPress function, onClick function, and the options object.

Here is a codesandbox with React 17.0.2 with a working example of useLongPress: https://codesandbox.io/s/uselongpress-forked-zmtem?file=/src/App.js

Kailash E
  • 151
  • 5
  • There's an issue - onClick never gets called. – Jon Aug 22 '21 at 03:47
  • Do you mean that you do not plan on needing an onClick action? – Kailash E Aug 22 '21 at 03:57
  • No it'd be nice if I knew when it was called, if I just add a regular onClick event - it gets called even during a longpress – Jon Aug 22 '21 at 04:04
  • On the codesandbox you can see that the onLongPress event is called when the user clicks and holds the click down for a given delay. The onClick event is called only when the button is clicked and released before the onLongPress could be called (basically a normal, fast click). – Kailash E Aug 22 '21 at 04:09
  • In the code original code for the question the line `shouldTriggerClick && !longPressTriggered && onClick(event);` is wrong since `longPressTriggered` is a ref if must be accessed via `longPressTriggered.current`. The code from @KailashE in codesandbox uses state and other minor fixes, that's why it works – Rodrigo Novais Jul 21 '23 at 17:30