React hooks - change state inside scroll event not working - reactjs

I try to change myState inside scroll handle function like
const [myState, setMyState] = useState(false);
const refA = useRef(null)
const handle = (e) => {
if (!myState) {
console.log('Set', myState)
setMyState(true);
}else {
console.log('no set')
}
}
useEffect(() => {
if (props.run) {
const ref= refA.current
ref.addEventListener("scroll", handle, { passive: true });
}
}, [props.run]);
useEffect(() => {
const ref= refA.current
return () => {
ref.removeEventListener("scroll", handle, { passive: true });
}
}, [])
return (
<div ref={refA}>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
</div>
);
But when i scroll my div, log is always show Set? myState can't change data? how to fix that thanks.

Just add myState in the useEffect. And add removeEventListener in return of this useEffect, don't need create other useEffect
useEffect(() => {
if (props.run) {
const ref = refA.current;
ref.addEventListener("scroll", handle, { passive: true });
return () => {
ref.removeEventListener("scroll", handle, { passive: true });
};
}
}, [props.run, myState]);
Add myState in the dependencies will call useEffect when this state change. And the variable in the handle will be updated with new state.
You should return removeEventListener after call addEventListener to make sure when useEffect call again, the old event will be removed.

Related

What is the good practice way to prevent a useEffect from triggering on initial render? [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

setTimeout React: too many re-renders

I'm trying to do something with setTimeout on a switch controller but I don't know what is the problem and I get this error when the code is run, this in fact is a custom hook I use: Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React from 'react';
const useVisibility = () => {
const [visibility, setVisibility] = React.useState(true);
const [firstTime, setFirstTime] = React.useState(true);
let timeOutId;
const removeTimer = () => {
clearTimeout(timeOutId);
timeOutId = 0;
};
React.useEffect(
() => {
document.addEventListener('visibilitychange', (e) => {
if (document.hidden) {
switch (firstTime) {
case true:
setFirstTime(false)
timeOutId = setTimeout(() => {
setVisibility(false);
}, 0);
break;
default:
timeOutId = setTimeout(() => {
setVisibility('closed');
}, 0);
break;
}
} else if (document.isConnected) {
removeTimer();
}
});
},
[visibility]
);
return { visibility, setVisibility };
};
export default useVisibility;
And here is how I'm using it, and also calling a React function inside it:
{
visibility === 'closed' ? <> {cheatingPrevent()}
<Modal onClose={() => setVisibility(true)}
title="test"
text="test." /> </> : null
}
React.useEffect will add an event listener to document every time visibility changes as you have it in the dependency array. For each visibilitychange event, all the duplicate event listeners added will run.
The problem with this is you're calling setVisibility in useEffect callback which updates visibility which in return re-runs useEffect.
You don't need visibility in dependency array of useEffect hook. Pass empty array []

Hide and Show modal on mouseenter and mouseleave using React Hooks

I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.

React useEffect is missing a depency error, but it is there

I have this component, and I am using useRef and useEffect to handle a click outside a popup so I can close it.
I have added the two dependencies the useEffect needs but I get this error:
The 'handleClickOutside' function makes the dependencies of useEffect Hook (at line 117) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'handleClickOutside' definition into its own useCallback()
As you can see here, I am adding both dependencies, but it still throws this error/warning:
useEffect(() => {
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen, handleClickOutside]);
Any ideas how to fix this?
Here i the codesandbox: https://codesandbox.io/s/laughing-newton-1gcme?fontsize=14&hidenavigation=1&theme=dark The problem is in src/components/typeahead line 100
And here the component code:
function ResultsOverlay({
isOpen,
items,
selectItem,
highlightedOption,
setIsOverlayOpen,
isOverlayOpen
}) {
const node = useRef();
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
setIsOverlayOpen(false);
};
useEffect(() => {
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen]);
function matchedOptionsClass(index) {
if (highlightedOption === index) {
return "ph4 list f3 sesame-red list-item pointer";
}
return "ph4 list sesame-blue list-item pointer";
}
if (isOpen) {
return (
<div className="absolute" ref={node}>
<ul className="w5 mt0 pa0 h5 overflow-scroll shadow-5 dib">
{items &&
items.map((item, index) => (
<li
onClick={() => selectItem(item)}
className={matchedOptionsClass(index)}
>
{item}
</li>
))}
</ul>
</div>
);
} else {
return null;
}
}
Two problems:
First, do what the linst is telling you and move your function definition inside your effect
useEffect(() => {
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
setIsOverlayOpen(false);
};
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen]);
Second, setIsOverlayOpen is a callback provided via props, so it doesn't have a stable signature and triggers the effect on each render.
Assuming that setIsOverlayOpen is a setter from useState and doesn't need to change it's signature you can workaround this by wrapping your handler in an aditional dependency check layer by using useCallback
const stableHandler = useCallback(setIsOverlayOpen, [])
useEffect(() => {
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
stableHandler(false);
};
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen, stableHandler]);

react long press event

Is there a way to add long press event in react-web application?
I have list of addresses. On long press on any address, I want to fire event to delete that address followed by a confirm box.
I've created a codesandbox with a hook to handle long press and click. Basically, on mouse down, touch start events, a timer is created with setTimeout. When the provided time elapses, it triggers long press.
On mouse up, mouse leave, touchend, etc, the timer is cleared.
useLongPress.js
import { useCallback, useRef, useState } from "react";
const useLongPress = (
onLongPress,
onClick,
{ shouldPreventDefault = true, delay = 300 } = {}
) => {
const [longPressTriggered, setLongPressTriggered] = useState(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);
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
const clear = useCallback(
(event, shouldTriggerClick = true) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick();
setLongPressTriggered(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;
To use the hook,
App.js
import useLongPress from "./useLongPress";
export default function App() {
const onLongPress = () => {
console.log('longpress is triggered');
};
const onClick = () => {
console.log('click is triggered')
}
const defaultOptions = {
shouldPreventDefault: true,
delay: 500,
};
const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);
return (
<div className="App">
<button {...longPressEvent}>use Loooong Press</button>
</div>
);
}
Older answer for class components:
You can use MouseDown, MouseUp, TouchStart, TouchEnd events to control timers that can act as a long press event. Check out the code below
class App extends Component {
constructor() {
super()
this.handleButtonPress = this.handleButtonPress.bind(this)
this.handleButtonRelease = this.handleButtonRelease.bind(this)
}
handleButtonPress () {
this.buttonPressTimer = setTimeout(() => alert('long press activated'), 1500);
}
handleButtonRelease () {
clearTimeout(this.buttonPressTimer);
}
render() {
return (
<div
onTouchStart={this.handleButtonPress}
onTouchEnd={this.handleButtonRelease}
onMouseDown={this.handleButtonPress}
onMouseUp={this.handleButtonRelease}
onMouseLeave={this.handleButtonRelease}>
Button
</div>
);
}
}
With hooks in react 16.8 you could rewrite class with functions and hooks.
import { useState, useEffect } from 'react';
export default function useLongPress(callback = () => {}, ms = 300) {
const [startLongPress, setStartLongPress] = useState(false);
useEffect(() => {
let timerId;
if (startLongPress) {
timerId = setTimeout(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [callback, ms, startLongPress]);
return {
onMouseDown: () => setStartLongPress(true),
onMouseUp: () => setStartLongPress(false),
onMouseLeave: () => setStartLongPress(false),
onTouchStart: () => setStartLongPress(true),
onTouchEnd: () => setStartLongPress(false),
};
}
import useLongPress from './useLongPress';
function MyComponent (props) {
const backspaceLongPress = useLongPress(props.longPressBackspaceCallback, 500);
return (
<Page>
<Button {...backspaceLongPress}>
Click me
</Button>
</Page>
);
};
Nice hook! But I would like make a small improvement. Using useCallback to wrap event handlers. This ensures these will not changed on every render.
import { useState, useEffect, useCallback } from 'react';
export default function useLongPress(callback = () => {}, ms = 300) {
const [startLongPress, setStartLongPress] = useState(false);
useEffect(() => {
let timerId;
if (startLongPress) {
timerId = setTimeout(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [callback, ms, startLongPress]);
const start = useCallback(() => {
setStartLongPress(true);
}, []);
const stop = useCallback(() => {
setStartLongPress(false);
}, []);
return {
onMouseDown: start,
onMouseUp: stop,
onMouseLeave: stop,
onTouchStart: start,
onTouchEnd: stop,
};
}
Based on #Sublime me comment above about avoiding multiple re-renders, my version doesn't use anything that triggers renders:
export function useLongPress({
onClick = () => {},
onLongPress = () => {},
ms = 300,
} = {}) {
const timerRef = useRef(false);
const eventRef = useRef({});
const callback = useCallback(() => {
onLongPress(eventRef.current);
eventRef.current = {};
timerRef.current = false;
}, [onLongPress]);
const start = useCallback(
(ev) => {
ev.persist();
eventRef.current = ev;
timerRef.current = setTimeout(callback, ms);
},
[callback, ms]
);
const stop = useCallback(
(ev) => {
ev.persist();
eventRef.current = ev;
if (timerRef.current) {
clearTimeout(timerRef.current);
onClick(eventRef.current);
timerRef.current = false;
eventRef.current = {};
}
},
[onClick]
);
return useMemo(
() => ({
onMouseDown: start,
onMouseUp: stop,
onMouseLeave: stop,
onTouchStart: start,
onTouchEnd: stop,
}),
[start, stop]
);
}
It also provides both onLongPress and onClick and passes on the event object received.
Usage is mostly as described earlier, except arguments are now passed in an object, all are optional:
const longPressProps = useLongPress({
onClick: (ev) => console.log('on click', ev.button, ev.shiftKey),
onLongPress: (ev) => console.log('on long press', ev.button, ev.shiftKey),
});
// and later:
return (<button {...longPressProps}>click me</button>);
Here is a Typescript version of the most popular answer, in case it is useful to anybody:
(it also fixes a problem with accessing event properties within the delegated event on the timeOut by using e.persist() and cloning the event)
useLongPress.ts
import { useCallback, useRef, useState } from "react";
function preventDefault(e: Event) {
if ( !isTouchEvent(e) ) return;
if (e.touches.length < 2 && e.preventDefault) {
e.preventDefault();
}
};
export function isTouchEvent(e: Event): e is TouchEvent {
return e && "touches" in e;
};
interface PressHandlers<T> {
onLongPress: (e: React.MouseEvent<T> | React.TouchEvent<T>) => void,
onClick?: (e: React.MouseEvent<T> | React.TouchEvent<T>) => void,
}
interface Options {
delay?: number,
shouldPreventDefault?: boolean
}
export default function useLongPress<T>(
{ onLongPress, onClick }: PressHandlers<T>,
{ delay = 300, shouldPreventDefault = true }
: Options
= {}
) {
const [longPressTriggered, setLongPressTriggered] = useState(false);
const timeout = useRef<NodeJS.Timeout>();
const target = useRef<EventTarget>();
const start = useCallback(
(e: React.MouseEvent<T> | React.TouchEvent<T>) => {
e.persist();
const clonedEvent = {...e};
if (shouldPreventDefault && e.target) {
e.target.addEventListener(
"touchend",
preventDefault,
{ passive: false }
);
target.current = e.target;
}
timeout.current = setTimeout(() => {
onLongPress(clonedEvent);
setLongPressTriggered(true);
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
const clear = useCallback((
e: React.MouseEvent<T> | React.TouchEvent<T>,
shouldTriggerClick = true
) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick?.(e);
setLongPressTriggered(false);
if (shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[shouldPreventDefault, onClick, longPressTriggered]
);
return {
onMouseDown: (e: React.MouseEvent<T>) => start(e),
onTouchStart: (e: React.TouchEvent<T>) => start(e),
onMouseUp: (e: React.MouseEvent<T>) => clear(e),
onMouseLeave: (e: React.MouseEvent<T>) => clear(e, false),
onTouchEnd: (e: React.TouchEvent<T>) => clear(e)
};
};
Generic hook that avoids re-renders
This is something I'm using in production, inspired by the original answers. If there's a bug below, well I guess I have a bug in production! 🤷‍♂️
Usage
I wanted to keep the hook a bit more concise and allow composability if the implementation calls for it (e.g.: adding fast input vs slow input, rather than a single callback).
const [onStart, onEnd] = useLongPress(() => alert('Old School Alert'), 1000);
return (
<button
type="button"
onTouchStart={onStart}
onTouchEnd={onEnd}
>
Hold Me (Touch Only)
</button>
)
Implementation
It's a simpler implementation than it seems. Just a lot more lines of comments.
I added a bunch of comments so if you do copy/paste this into your codebase, your colleagues can understand it better during PR.
import {useCallback, useRef} from 'react';
export default function useLongPress(
// callback that is invoked at the specified duration or `onEndLongPress`
callback : () => any,
// long press duration in milliseconds
ms = 300
) {
// used to persist the timer state
// non zero values means the value has never been fired before
const timerRef = useRef<number>(0);
// clear timed callback
const endTimer = () => {
clearTimeout(timerRef.current || 0);
timerRef.current = 0;
};
// init timer
const onStartLongPress = useCallback((e) => {
// stop any previously set timers
endTimer();
// set new timeout
timerRef.current = window.setTimeout(() => {
callback();
endTimer();
}, ms);
}, [callback, ms]);
// determine to end timer early and invoke the callback or do nothing
const onEndLongPress = useCallback(() => {
// run the callback fn the timer hasn't gone off yet (non zero)
if (timerRef.current) {
endTimer();
callback();
}
}, [callback]);
return [onStartLongPress, onEndLongPress, endTimer];
}
Example
Using 500ms setting in the example. The spontaneous circle in the GIF shows when I'm pressing down.
Here's a component that provides onClick and onHold events - adapt as needed...
CodeSandbox: https://codesandbox.io/s/hold-press-event-r8q9w
Usage:
import React from 'react'
import Holdable from './holdable'
function App() {
function onClick(evt) {
alert('click ' + evt.currentTarget.id)
}
function onHold(evt) {
alert('hold ' + evt.currentTarget.id)
}
const ids = 'Label1,Label2,Label3'.split(',')
return (
<div className="App">
{ids.map(id => (
<Holdable
onClick={onClick}
onHold={onHold}
id={id}
key={id}
>
{id}
</Holdable>
))}
</div>
)
}
holdable.jsx:
import React from 'react'
const holdTime = 500 // ms
const holdDistance = 3**2 // pixels squared
export default function Holdable({id, onClick, onHold, children}) {
const [timer, setTimer] = React.useState(null)
const [pos, setPos] = React.useState([0,0])
function onPointerDown(evt) {
setPos([evt.clientX, evt.clientY]) // save position for later
const event = { ...evt } // convert synthetic event to real object
const timeoutId = window.setTimeout(timesup.bind(null, event), holdTime)
setTimer(timeoutId)
}
function onPointerUp(evt) {
if (timer) {
window.clearTimeout(timer)
setTimer(null)
onClick(evt)
}
}
function onPointerMove(evt) {
// cancel hold operation if moved too much
if (timer) {
const d = (evt.clientX - pos[0])**2 + (evt.clientY - pos[1])**2
if (d > holdDistance) {
setTimer(null)
window.clearTimeout(timer)
}
}
}
function timesup(evt) {
setTimer(null)
onHold(evt)
}
return (
<div
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onPointerMove={onPointerMove}
id={id}
>
{children}
</div>
)
}
Note: this doesn't work with Safari yet - pointer events are coming in v13 though - https://caniuse.com/#feat=pointer
This is the simplest and best solution I could made on my own.
This way you don't need to pass the click event
Click event still working
The hook returns a function instead of the events itselves , then you can use it within a loop or conditionally and pass different callbacks to each element.
useLongPress.js
export default function useLongPress() {
return function (callback) {
let timeout;
let preventClick = false;
function start() {
timeout = setTimeout(() => {
preventClick = true;
callback();
}, 300);
}
function clear() {
timeout && clearTimeout(timeout);
preventClick = false;
}
function clickCaptureHandler(e) {
if (preventClick) {
e.stopPropagation();
preventClick = false;
}
}
return {
onMouseDown: start,
onTouchStart: start,
onMouseUp: clear,
onMouseLeave: clear,
onTouchMove: clear,
onTouchEnd: clear,
onClickCapture: clickCaptureHandler
};
}
}
Usage:
import useLongPress from './useLongPress';
export default function MyComponent(){
const onLongPress = useLongPress();
const buttons = ['button one', 'button two', 'button three'];
return (
buttons.map(text =>
<button
onClick={() => console.log('click still working')}
{...onLongPress(() => console.log('long press worked for ' + text))}
>
{text}
</button>
)
)
}
Brian's solution allows you to pass params to the children which I think is not doable with the Hook. Still, if I may suggest a bit cleaner solution for most common case where you want to add onHold behavior to a single component and you also want to be able to change the onHold timeout.
Material-UI example with Chip component:
'use strict';
const {
Chip
} = MaterialUI
function ChipHoldable({
onClick = () => {},
onHold = () => {},
hold = 500,
...props
}) {
const [timer, setTimer] = React.useState(null);
function onPointerDown(evt) {
const event = { ...evt
}; // convert synthetic event to real object
const timeoutId = window.setTimeout(timesup.bind(null, event), hold);
setTimer(timeoutId);
}
function onPointerUp(evt) {
if (timer) {
window.clearTimeout(timer);
setTimer(null);
onClick(evt);
}
}
const onContextMenu = e => e.preventDefault();
const preventDefault = e => e.preventDefault(); // so that ripple effect would be triggered
function timesup(evt) {
setTimer(null);
onHold(evt);
}
return React.createElement(Chip, {
onPointerUp,
onPointerDown,
onContextMenu,
onClick: preventDefault,
...props
});
}
const App = () => <div> {[1,2,3,4].map(i => < ChipHoldable style={{margin:"10px"}}label = {`chip${i}`}
onClick = {
() => console.log(`chip ${i} clicked`)
}
onHold = {
() => console.log(`chip ${i} long pressed`)
}
/>)}
</div>
ReactDOM.render( <App/>, document.querySelector('#root'));
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<script src="https://unpkg.com/#material-ui/core#latest/umd/material-ui.development.js"></script>
</body>
</html>
An adaptation of David's solution: a React hook for when you want to repeatedly fire the event. It uses setInterval instead.
export function useHoldPress(callback = () => {}, ms = 300) {
const [startHoldPress, setStartHoldPress] = useState(false);
useEffect(() => {
let timerId;
if (startHoldPress) {
timerId = setInterval(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [startHoldPress]);
return {
onMouseDown: () => setStartHoldPress(true),
onMouseUp: () => setStartHoldPress(false),
onMouseLeave: () => setStartHoldPress(false),
onTouchStart: () => setStartHoldPress(true),
onTouchEnd: () => setStartHoldPress(false)
};
}
Ionic React LongPress Example
I use it with Ionic React, it works well.
import React, {useState} from 'react';
import { Route, Redirect } from 'react-router';
interface MainTabsProps { }
const MainTabs: React.FC<MainTabsProps> = () => {
// timeout id
var initial: any;
// setstate
const [start, setStart] = useState(false);
const handleButtonPress = () => {
initial = setTimeout(() => {
setStart(true); // start long button
console.log('long press button');
}, 1500);
}
const handleButtonRelease = () => {
setStart(false); // stop long press
clearTimeout(initial); // clear timeout
if(start===false) { // is click
console.log('click button');
}
}
return (
<IonPage>
<IonHeader>
<IonTitle>Ionic React LongPress</IonTitle>
</IonHeader>
<IonContent className="ion-padding">
<IonButton expand="block"
onMouseDown={handleButtonPress}
onMouseUp={handleButtonRelease} >LongPress</IonButton>
</IonContent>
</IonPage>
);
};
export default MainTabs;
Just wanted to point out that hooks aren't a great solution here since you can't use them in a call back.
for example, if you wanted to add long press to a number of elements:
items.map(item => <button {...useLongPress(() => handle(item))}>{item}</button>)
gets you:
... React Hooks must be called in a React function component or a
custom React Hook function
you could however use vanilla JS:
export default function longPressEvents(callback, ms = 500) {
let timeout = null
const start = () => timeout = setTimeout(callback, ms)
const stop = () => timeout && window.clearTimeout(timeout)
return callback ? {
onTouchStart: start,
onTouchMove: stop,
onTouchEnd: stop,
} : {}
}
then:
items.map(item => <button { ...longPressEvents(() => handle(item)) }>{item}</button>)
demo: https://codesandbox.io/s/long-press-hook-like-oru24?file=/src/App.js
just be aware that longPressEvents will run every render. Probably not a big deal, but something to keep in mind.
Type Script example make common long Press event
import { useCallback, useRef, useState } from "react";
interface Props {
onLongPress: (e: any) => void;
onClick: (e: any) => void;
obj: { shouldPreventDefault: boolean, delay: number }
}
const useLongPress = (props: Props) => {
const [longPressTriggered, setLongPressTriggered] = useState(false);
const timeout: any = useRef();
const target: any = useRef();
const start = useCallback(
event => {
if (props.obj.shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, {
passive: false
});
target.current = event.target;
}
timeout.current = setTimeout(() => {
props.onLongPress(event);
setLongPressTriggered(true);
}, props.obj.delay);
},
[props]
);
const clear = useCallback(
(event, shouldTriggerClick = true) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && props.onClick(event);
setLongPressTriggered(false);
if (props.obj.shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[longPressTriggered, props]
);
return {
onMouseDown: (e: any) => start(e),
onTouchStart: (e: any) => start(e),
onMouseUp: (e: any) => clear(e),
onMouseLeave: (e: any) => clear(e, false),
onTouchEnd: (e: any) => clear(e)
};
};
const isTouchEvent = (event: any) => {
return "touches" in event;
};
const preventDefault = (event: any) => {
if (!isTouchEvent(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};
export default useLongPress;
Use of that common function
import useLongPress from "shared/components/longpress";
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,
};
<div {...longPressEvent}></div>
After much deliberation, looking at other answers, and adding newer features, I think I now have a solid, if not the best, React long press implementation yet. Here are the highlights:
Only one fn needs to be passed that will be used for both onClick and onLongPress, though they can still be individually defined
Stores the fn in a ref so you can do state updates without having to worry about the fn going stale and not getting the latest react state
Allows for a static or dynamic delay so the longPress fn can start to execute faster or slower depending on how long the button has been held
Written in typescript
// useInterval.ts
import React from "react";
export default function useInterval(callback: any, delay: number | null) {
const savedCallback = React.useRef<any>();
React.useEffect(() => {
savedCallback.current = callback;
});
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// useLongPress.ts
import React from "react";
import useInterval from "./use-interval";
type Fn<T> = (
e: React.MouseEvent<T, MouseEvent>,
pressedTimeElapsedInMs: number
) => void;
type Opts<T extends HTMLElement> = {
shouldPreventDefault?: boolean;
delay?: number | ((pressedTimeElapsedInMs: number) => number);
onClick?: boolean | Fn<T>;
};
/**
* useLongPress hook that handles onClick and longPress events.
* if you dont pass an onClick fn, the longPress fn will be for onClick.
* the delay can be a number or a function that recieves how long the button has been pressed.
* This value can be used to calculate a dynamic value.
* The onClick and longPress fns will receive the click or touch event as the first parameter,
* and how long the button has been pressed as the second parameter.
* #param onLongPress
* #param opts
* #returns
*/
export default function useLongPress<T extends HTMLElement>(
onLongPress: Fn<T>,
opts: Opts<T> = {}
) {
const {
// default onClick to onLongPress if no onClick fn is provided
onClick = onLongPress,
shouldPreventDefault = true,
delay: initialDelay = 300,
} = opts;
// hold duration in ms
const [holdDuration, setHoldDuration] = React.useState(0);
const [longPressTriggered, setLongPressTriggered] = React.useState(false);
const [delay, setDelay] = React.useState(0);
const target = React.useRef<EventTarget | null>(null);
// store the click or touch event globally so the fn function can pass it to longPress
const evt = React.useRef<any | null>(null);
// store the latest onLongPress and onClick fns here to prevent them being stale when used
const longPressRef = React.useRef<Fn<T>>();
const clickRef = React.useRef<Fn<T>>();
// update the onClick and onLongPress fns everytime they change
React.useEffect(() => {
longPressRef.current = onLongPress;
// if false is passed as onClick option, use onLongPress fn in its place
clickRef.current = typeof onClick === "boolean" ? onLongPress : onClick;
}, [onClick, onLongPress]);
// this fn will be called onClick and in on interval when the btn is being held down
const fn = React.useCallback(() => {
// call the passed in onLongPress fn, giving it the click
// event and the length of time the btn is being held
longPressRef.current?.(evt.current, holdDuration);
// get the latest delay duration by passing the current
// hold duration if it was a fn, or just use the number
const updatedDelay =
typeof initialDelay === "function"
? initialDelay(holdDuration)
: initialDelay;
// update the delay if its dynamic
setDelay(updatedDelay);
// update how long the btn has been pressed
setHoldDuration(holdDuration + updatedDelay);
setLongPressTriggered(true);
}, [initialDelay, holdDuration]);
// start calling the fn function on an interval as the button is being held
useInterval(fn, longPressTriggered ? delay : null);
// this fn is called onMouseDown and onTouchStart
const start = React.useCallback(
(event: React.MouseEvent<T, MouseEvent> | React.TouchEvent<T>) => {
if (shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, {
passive: false,
});
target.current = event.target;
}
// globally store the click event
evt.current = event;
// call the fn function once, which handles the onClick
fn();
},
[shouldPreventDefault, fn]
);
// this fn is called onMouseUp and onTouchEnd
const clear = React.useCallback(
(
event: React.MouseEvent<T, MouseEvent> | React.TouchEvent<T>,
shouldTriggerClick = true
) => {
// reset how long the btn has been held down
setHoldDuration(0);
if (shouldTriggerClick && !longPressTriggered) {
clickRef.current?.(
event as React.MouseEvent<T, MouseEvent>,
holdDuration
);
}
// stop the interval
setLongPressTriggered(false);
// clear the globally stored click event
evt.current = null;
if (shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[clickRef, longPressTriggered, shouldPreventDefault, holdDuration]
);
return {
onMouseDown: (e: React.MouseEvent<T, MouseEvent>) => start(e),
onMouseUp: (e: React.MouseEvent<T, MouseEvent>) => clear(e),
onMouseLeave: (e: React.MouseEvent<T, MouseEvent>) => clear(e, false),
onTouchStart: (e: React.TouchEvent<T>) => start(e),
onTouchEnd: (e: React.TouchEvent<T>) => clear(e),
};
}
const assertTouchEvt = (event: Event | TouchEvent): event is TouchEvent => {
return "touches" in event;
};
const preventDefault = (event: Event | TouchEvent) => {
if (!assertTouchEvt(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};
Then the hook can be used in the following ways:
state update with default options
export default App() {
const [count, setCount] = React.useState(0)
const useIncrement = useLongPress((e, holdDurationInMs) => {
setCount(count + 1)
})
}
state update with a static delay and where the amount increases based on how many milliseconds the button has been held down
export default App() {
const [count, setCount] = React.useState(0)
const useIncrement = useLongPress((e, holdDurationInMs) => {
if (holdDurationInMs < 1000) setCount(count + (e.metaKey || e.shiftKey ? 5 : 1))
else if (holdDurationInMs < 3000) setCount(count + 5)
else setCount(count + 100)
}, {
delay: 300
})
}
state update with a dynamic delay that executes the function faster as the button is held down longer
export default App() {
const [count, setCount] = React.useState(0)
const useIncrement = useLongPress((e, holdDurationInMs) => {
setCount(count + 1)
}, {
delay: (holdDurationInMs) => {
if (holdDurationInMs < 1000) return 550;
else if (holdDurationInMs < 3000) return 450;
else if (holdDurationInMs < 8000) return 250;
else return 110;
},
})
}
Thanks, #sudo bangbang for this great custom hook.
I had some problems, though:
When I was scrolling through a table with a mobile device (touch input), this hook accidentally triggered a click during the scrolling. Of course, this is not want we want.
Another problem was if I was scrolling very slowly, the hook accidentally triggered the long press.
I managed to circumvent this behavior with subtle changes:
// Set 'shouldPreventDefault' to false to listen also to 'onMouseUp',
// would be canceled otherwise if 'shouldPreventDefault' would have been 'true'
const defaultOptions = { shouldPreventDefault: false, delay: 500 };
return {
onMouseDown: (e) => start(e),
onTouchStart: (e) => start(e),
onMouseUp: (e) => clear(e),
onMouseLeave: (e) => clear(e, false),
onTouchEnd: (e) => clear(e, false), // Do not trigger click here
onTouchMove: (e) => clear(e, false), // Do not trigger click here
};
Here is my implementation with the modifications
import { useCallback, useRef, useState } from "react";
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Custom hook to handle a long press event (e.g. on mobile for secondary action)
// https://stackoverflow.com/a/48057286/7220665
// Usage:
// const onLongPress = () => {console.info('long press is triggered')};
// const onClick = () => {console.info('click is triggered')};
// const defaultOptions = { shouldPreventDefault: false, delay: 500 };
// const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);
// return <button {...longPressEvent}>do long Press</button>
//
// If we are scrolling with the finger 'onTouchStart' and 'onTouchEnd' is triggered
// if we are clicking with the finger additionally to 'onTouchStart' and 'onTouchEnd' ->
// 'onMouseDown' 'onMouseUp' is triggered as well
// We do not want a click event if the user is just scrolling (e.g. in a list or table)
// That means 'onTouchEnd' should not trigger a click
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Hook
const useLongPress = (onLongPress, onClick, { shouldPreventDefault = true, delay = 300 } = {}) => {
// console.info("useLongPress");
const [longPressTriggered, setLongPressTriggered] = useState(false);
const timeout = useRef();
const target = useRef();
//
// Start the long press if 'onMouseDown' or 'onTouchStart'
const start = useCallback(
(event) => {
console.info("useLongPress start");
// Create listener
if (shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, { passive: false });
target.current = event.target;
}
// A long press event has been triggered
timeout.current = setTimeout(() => {
onLongPress(event);
setLongPressTriggered(true);
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
//
// Clear the long press if 'onMouseUp', 'onMouseLeave' or 'onTouchEnd'
const clear = useCallback(
(event, shouldTriggerClick = true) => {
console.info("useLongPress clear event:", event);
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick(event);
setLongPressTriggered(false);
// Create listener
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, false), // Do not trigger click here
onTouchMove: (e) => clear(e, false), // Do not trigger click here
};
};
//
// Check if it is a touch event - called by 'preventDefault'
const isTouchEvent = (event) => {
console.info("useLongPress isTouchEvent");
return "touches" in event;
};
//
//
const preventDefault = (event) => {
console.info("useLongPress preventDefault");
if (!isTouchEvent(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
if (event.cancelable) event.preventDefault();
}
};
export default useLongPress;
Now a click is NOT triggered onTouchUp (which will be called if we are scrolling through a list or table) but onMouseUp, which will be triggered additionally to onTouchUp if we are scrolling (although we are not really using a mouse)

Resources