Reputation: 2069
I am building a Minesweeper game with React and want to perform a different action when a cell is single or double clicked. Currently, the onDoubleClick
function will never fire, the alert from onClick
is shown. If I remove the onClick
handler, onDoubleClick
works. Why don't both events work? Is it possible to have both events on an element?
/** @jsx React.DOM */
var Mine = React.createClass({
render: function(){
return (
<div className="mineBox" id={this.props.id} onDoubleClick={this.props.onDoubleClick} onClick={this.props.onClick}></div>
)
}
});
var MineRow = React.createClass({
render: function(){
var width = this.props.width,
row = [];
for (var i = 0; i < width; i++){
row.push(<Mine id={String(this.props.row + i)} boxClass={this.props.boxClass} onDoubleClick={this.props.onDoubleClick} onClick={this.props.onClick}/>)
}
return (
<div>{row}</div>
)
}
})
var MineSweeper = React.createClass({
handleDoubleClick: function(){
alert('Double Clicked');
},
handleClick: function(){
alert('Single Clicked');
},
render: function(){
var height = this.props.height,
table = [];
for (var i = 0; i < height; i++){
table.push(<MineRow width={this.props.width} row={String.fromCharCode(97 + i)} onDoubleClick={this.handleDoubleClick} onClick={this.handleClick}/>)
}
return (
<div>{table}</div>
)
}
})
var bombs = ['a0', 'b1', 'c2'];
React.renderComponent(<MineSweeper height={5} width={5} bombs={bombs}/>, document.getElementById('content'));
Upvotes: 128
Views: 186579
Reputation: 1161
I created a simple solution that you can use in your onClick handler with props in typescript. This is handy for if you want to use it in a list (my use case). You could also use the event.detail instead of a state but a state is fine in most cases. You could also make props undefined etc.
export function debounce(
func: (...args: unknown[]) => void,
wait: number,
): (...args: unknown[]) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: unknown[]) => {
const later = () => {
timeoutId = null;
func(...args);
};
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(later, wait);
};
}
type UseDoubleClickHandlers<T> = {
onClick?: (props: T) => void;
onDoubleClick?: (props: T) => void;
};
export function useDoubleClick(delay = 150) {
const clickCountRef = useRef(0);
const clickHandler = <T>(props: T, handlers: UseDoubleClickHandlers<T>) => {
if (clickCountRef.current === 1) {
handlers.onClick?.(props);
}
if (clickCountRef.current > 1) {
handlers.onDoubleClick?.(props);
}
clickCountRef.current = 0;
};
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
const debouncedClickHandler = useMemo(() => debounce(clickHandler, delay), []);
return <T>(props: T, handlers: UseDoubleClickHandlers<T>) =>
(e: ReactMouseEvent<Element, MouseEvent>) => {
clickCountRef.current += 1;
debouncedClickHandler(props, handlers);
};
}
function YourReactComponent() {
const onDoubleClickHandler = useDoubleClick();
return (
<>
{[1, 2, 3].map(number => {
return <div key={number} onClick={onDoubleClickHandler(number, {
onClick: () => console.log('Single click'),
onDoubleClick: () => console.log('Double click'),
})}>{number}</div>;
})}
</>
);
}
Upvotes: 0
Reputation: 7630
Thank you '@Erminea Nea' for your great answer.
I just wanted to share my improvement to that hook, adding a return ref of the executed event:
import { useState, useEffect, MouseEvent } from 'react';
const useSingleAndDoubleClick = (
actionSimpleClick: (e: MouseEvent) => void,
actionDoubleClick: (e: MouseEvent) => void,
delay = 250
) => {
const [state, setState] = useState<{ click: number; e: MouseEvent | null }>({ click: 0, e: null });
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (state.click === 1) actionSimpleClick(state.e!);
setState({ e: state.e, click: 0 });
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (state.click === 2) actionDoubleClick(state.e!);
return () => clearTimeout(timer);
}, [state, actionSimpleClick, actionDoubleClick, delay]);
return (e: MouseEvent) => {
setState({ click: state.click + 1, e });
};
};
export default useSingleAndDoubleClick;
And the usage:
const onSingleClick = (e?: MouseEvent): void => {
console.log('single click');
e && e.stopPropagation();
};
const onDoubleClick = (e?: MouseEvent): void => {
console.log('double click');
e && e.stopPropagation();
};
const onVideoClick = useSingleAndDoubleClick(onSingleClick, onDoubleClick);
return (<div onClick={onVideoClick}> Click me</div>);
Upvotes: 3
Reputation: 121
Based on the solution by Erminea Nea and the improvements by Stanislau Buzunko (great work! 👏), I just created yet another solution that allows handling an arbitrary number of clicks and passes all original arguments, including the event
object.
import { useState, useEffect } from 'react';
function useMultiClickHandler(handler, delay = 400) {
const [state, setState] = useState({ clicks: 0, args: [] });
useEffect(() => {
const timer = setTimeout(() => {
setState({ clicks: 0, args: [] });
if (state.clicks > 0 && typeof handler[state.clicks] === 'function') {
handler[state.clicks](...state.args);
}
}, delay);
return () => clearTimeout(timer);
}, [handler, delay, state.clicks, state.args]);
return (...args) => {
setState((prevState) => ({ clicks: prevState.clicks + 1, args }));
if (typeof handler[0] === 'function') {
handler[0](...args);
}
};
}
Usage: You simply pass an object with keys matching the number of clicks required and the handler to fire as their respective value. If you want to fire an handler on any click, use the 0
key.
const onMultiClick = useMultiClickHandler({
0: (event) => console.log('Click', event),
1: (event) => console.log('Single click', event),
2: (event) => console.log('Double click', event),
6: (event) => console.log('Sixth click', event)
});
<button onClick={onMultiClick}>Click me</button>
Clicking the button six times will log "Click" six times, and "Sixth click" once about 400 ms after the last click.
The solution above even supports passing arbitrary properties:
const onMultiClick = useMultiClickHandler({
1: (event, ...args) => console.log('Single click', event, args),
2: (event, ...args) => console.log('Double click', event, args)
});
<button onClick={(event) => onMultiClick(event, some, other, param)}>Click me</button>
Since the event
object is passed too, you can still access e.g. event.button
. However, keep in mind that functions like event.stopPropagation()
won't work within the single and double click handlers, because the event has long been propagated when they are called. These functions (also including e.g. event.preventDefault()
) can only be used within the "click" handler (the handler with key 0
). I'd say this truly is a browser limitation.
Upvotes: 1
Reputation: 2553
If you want to conditionally handle both single-click and double-click, you can use this code:
function MyComponent() {
const clickTimeout = useRef();
function handleClick() {
clearTimeout(clickTimeout.current);
clickTimeout.current = setTimeout(() => {
console.log("1");
}, 250);
}
function handleDoubleClick() {
clearTimeout(clickTimeout.current);
console.log("2");
}
return (
<button onClick={handleClick} onDoubleClick={handleDoubleClick}>Click</button>
)
}
Here is the Demo:
https://codesandbox.io/embed/codepen-with-react-forked-5dpbei
Upvotes: 0
Reputation: 647
Typescript React hook to capture both single and double clicks, inspired by @erminea-nea 's answer:
import {useEffect, useState} from "react";
export function useSingleAndDoubleClick(
handleSingleClick: () => void,
handleDoubleClick: () => void,
delay = 250
) {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
if (click === 1) {
handleSingleClick();
}
setClick(0);
}, delay);
if (click === 2) {
handleDoubleClick();
}
return () => clearTimeout(timer);
}, [click, handleSingleClick, handleDoubleClick, delay]);
return () => setClick(prev => prev + 1);
}
Usage:
<span onClick={useSingleAndDoubleClick(
() => console.log('single click'),
() => console.log('double click')
)}>click</span>
Upvotes: 2
Reputation: 1
import React, { useState } from "react";
const List = () => {
const [cv, uv] = useState("nice");
const ty = () => {
uv("bad");
};
return (
<>
<h1>{cv}</h1>
<button onDoubleClick={ty}>Click to change</button>
</>
);
};
export default List;
Upvotes: -1
Reputation: 1831
I've updated Erminea Nea solution with passing an original event so that you can stop propagation + in my case I needed to pass dynamic props to my 1-2 click handler. All credit goes to Erminea Nea.
Here is a hook I've come up with:
import { useState, useEffect } from 'react';
const initialState = {
click: 0,
props: undefined
}
function useSingleAndDoubleClick(actionSimpleClick, actionDoubleClick, delay = 250) {
const [state, setState] = useState(initialState);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (state.click === 1) actionSimpleClick(state.props);
setState(initialState);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (state.click === 2) actionDoubleClick(state.props);
return () => clearTimeout(timer);
}, [state.click]);
return (e, props) => {
e.stopPropagation()
setState(prev => ({
click: prev.click + 1,
props
}))
}
}
export default useSingleAndDoubleClick
Usage in some component:
const onClick = useSingleAndDoubleClick(callbackClick, callbackDoubleClick)
<button onClick={onClick}>Click me</button>
or
<button onClick={e => onClick(e, someOtherProps)}>Click me</button>
Upvotes: 0
Reputation: 4291
Here's my solution for React in TypeScript:
import { debounce } from 'lodash';
const useManyClickHandlers = (...handlers: Array<(e: React.UIEvent<HTMLElement>) => void>) => {
const callEventHandler = (e: React.UIEvent<HTMLElement>) => {
if (e.detail <= 0) return;
const handler = handlers[e.detail - 1];
if (handler) {
handler(e);
}
};
const debounceHandler = debounce(function(e: React.UIEvent<HTMLElement>) {
callEventHandler(e);
}, 250);
return (e: React.UIEvent<HTMLElement>) => {
e.persist();
debounceHandler(e);
};
};
And an example use of this util:
const singleClickHandler = (e: React.UIEvent<HTMLElement>) => {
console.log('single click');
};
const doubleClickHandler = (e: React.UIEvent<HTMLElement>) => {
console.log('double click');
};
const clickHandler = useManyClickHandlers(singleClickHandler, doubleClickHandler);
// ...
<div onClick={clickHandler}>Click me!</div>
Upvotes: 1
Reputation: 449
You can use a custom hook to handle simple click and double click like this :
import { useState, useEffect } from 'react';
function useSingleAndDoubleClick(actionSimpleClick, actionDoubleClick, delay = 250) {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick(prev => prev + 1);
}
then in your component you can use :
const click = useSingleAndDoubleClick(callbackClick, callbackDoubleClick);
<button onClick={click}>clic</button>
Upvotes: 34
Reputation: 81370
Instead of using ondoubleclick
, you can use event.detail
to get the current click count. It's the number of time the mouse's been clicked in the same area in a short time.
const handleClick = (e) => {
switch (e.detail) {
case 1:
console.log("click");
break;
case 2:
console.log("double click");
break;
case 3:
console.log("triple click");
break;
}
};
return <button onClick={handleClick}>Click me</button>;
In the example above, if you triple click the button it will print all 3 cases:
click
double click
triple click
Upvotes: 75
Reputation: 752
Here is one way to achieve the same with promises. waitForDoubleClick
returns a Promise
which will resolve only if double click was not executed. Otherwise it will reject. Time can be adjusted.
async waitForDoubleClick() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (!this.state.prevent) {
resolve(true);
} else {
reject(false);
}
}, 250);
this.setState({ ...this.state, timeout, prevent: false })
});
}
clearWaitForDoubleClick() {
clearTimeout(this.state.timeout);
this.setState({
prevent: true
});
}
async onMouseUp() {
try {
const wait = await this.waitForDoubleClick();
// Code for sinlge click goes here.
} catch (error) {
// Single click was prevented.
console.log(error)
}
}
Upvotes: 0
Reputation: 141
This is the solution of a like button with increment and discernment values based on solution of Erminea.
useEffect(() => {
let singleClickTimer;
if (clicks === 1) {
singleClickTimer = setTimeout(
() => {
handleClick();
setClicks(0);
}, 250);
} else if (clicks === 2) {
handleDoubleClick();
setClicks(0);
}
return () => clearTimeout(singleClickTimer);
}, [clicks]);
const handleClick = () => {
console.log('single click');
total = totalClicks + 1;
setTotalClicks(total);
}
const handleDoubleClick = () => {
console.log('double click');
if (total > 0) {
total = totalClicks - 1;
}
setTotalClicks(total);
}
return (
<div
className="likeButton"
onClick={() => setClicks(clicks + 1)}
>
Likes | {totalClicks}
</div>
)
Upvotes: 0
Reputation: 121
Here's what I have done. Any suggestions for improvement are welcome.
class DoubleClick extends React.Component {
state = {counter: 0}
handleClick = () => {
this.setState(state => ({
counter: this.state.counter + 1,
}))
}
handleDoubleClick = () => {
this.setState(state => ({
counter: this.state.counter - 2,
}))
}
render() {
return(
<>
<button onClick={this.handleClick} onDoubleClick={this.handleDoubleClick>
{this.state.counter}
</button>
</>
)
}
}
Upvotes: 4
Reputation: 401
The required result can be achieved by providing a very slight delay on firing off the normal click action, which will be cancelled when the double click event will happen.
let timer = 0;
let delay = 200;
let prevent = false;
doClickAction() {
console.log(' click');
}
doDoubleClickAction() {
console.log('Double Click')
}
handleClick() {
let me = this;
timer = setTimeout(function() {
if (!prevent) {
me.doClickAction();
}
prevent = false;
}, delay);
}
handleDoubleClick(){
clearTimeout(timer);
prevent = true;
this.doDoubleClickAction();
}
< button onClick={this.handleClick.bind(this)}
onDoubleClick = {this.handleDoubleClick.bind(this)} > click me </button>
Upvotes: 40
Reputation: 8314
Edit:
I've found that this is not an issue with React 0.15.3.
Original:
For React 0.13.3, here are two solutions.
Note, even in the case of double-click, the single-click handler will be called twice (once for each click).
const ListItem = React.createClass({
handleClick() {
console.log('single click');
},
handleDoubleClick() {
console.log('double click');
},
refCallback(item) {
if (item) {
item.getDOMNode().ondblclick = this.handleDoubleClick;
}
},
render() {
return (
<div onClick={this.handleClick}
ref={this.refCallback}>
</div>
);
}
});
module.exports = ListItem;
I had another solution that used lodash
, but I abandoned it because of the complexity. The benefit of this was that "click" was only called once, and not at all in the case of "double-click".
import _ from 'lodash'
const ListItem = React.createClass({
handleClick(e) {
if (!this._delayedClick) {
this._delayedClick = _.debounce(this.doClick, 500);
}
if (this.clickedOnce) {
this._delayedClick.cancel();
this.clickedOnce = false;
console.log('double click');
} else {
this._delayedClick(e);
this.clickedOnce = true;
}
},
doClick(e) {
this.clickedOnce = undefined;
console.log('single click');
},
render() {
return (
<div onClick={this.handleClick}>
</div>
);
}
});
module.exports = ListItem;
I appreciate the idea that double-click isn't something easily detected, but for better or worse it IS a paradigm that exists and one that users understand because of its prevalence in operating systems. Furthermore, it's a paradigm that modern browsers still support. Until such time that it is removed from the DOM specifications, my opinion is that React should support a functioning onDoubleClick
prop alongside onClick
. It's unfortunate that it seems they do not.
Upvotes: 19
Reputation: 44880
This is not a limitation of React, it is a limitation of the DOM's click
and dblclick
events. As suggested by Quirksmode's click documentation:
Don't register click and dblclick events on the same element: it's impossible to distinguish single-click events from click events that lead to a dblclick event.
For more current documentation, the W3C spec on the dblclick
event states:
A user agent must dispatch this event when the primary button of a pointing device is clicked twice over an element.
A double click event necessarily happens after two click events.
Edit:
One more suggested read is jQuery's dblclick
handler:
It is inadvisable to bind handlers to both the click and dblclick events for the same element. The sequence of events triggered varies from browser to browser, with some receiving two click events before the dblclick and others only one. Double-click sensitivity (maximum time between clicks that is detected as a double click) can vary by operating system and browser, and is often user-configurable.
Upvotes: 78