116

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'));
davidism
  • 121,510
  • 29
  • 395
  • 339
thisisnotabus
  • 1,949
  • 3
  • 15
  • 31
  • 3
    Because an alert pops up after the first click, you are never able to perform the second click to trigger the `dblclick` event. See my answer for why it's not possible to prevent the first click. – Ross Allen Sep 11 '14 at 06:43
  • How about a workaround? Track the time between clicks, and consider double-click for any two clicks that happens under a certain duration, like 600ms. – kayleeFrye_onDeck Dec 15 '15 at 18:04
  • 1
    This is a UI design issue. The gesture you PROBABLY want is long-press (which you will have to synthesize yourself from mouse/touch events). Delaying the click action for a possible pending double-click leads to a really unpleasant user-experience. Not so the long-press gesture. – Robin Davies Feb 28 '22 at 11:29
  • Ironically, I think I've played your game. lol. Using single-click/double-click is a completely unusuble UI conventin for minesweeper. So much so that it made a lasting negative impression. So many gratuitous explosions on what was *supposed* to have been an attempt to put down a flag! Long press might be better -- especially if you can provide a haptic affordance or a sound to indicate "long-press complete". – Robin Davies Feb 28 '22 at 12:00

13 Answers13

74

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.

Ross Allen
  • 43,772
  • 14
  • 97
  • 95
  • 4
    Fatuously stupid comments. From a UI design perspective, it is inadvisable to have a dbl-click handler for an element that does not ALSO have a click handler (otherwise just use click). Under any conceivable UI convention that uses dblClick, one must NOT eat the click that precedes the dblClick (usually one performs a selection). The jQuery comment is only slightly more useful,; one does needs to be aware that on some platforms, one may receive click/dblClick, and on others click/click/DblClick. (And it actually *is* a limitation of react -- re-rendering selection state kills the dblClick). – Robin Davies Feb 28 '22 at 11:23
53

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 

Live Demo

Edit 25777826/onclick-works-but-ondoubleclick-is-ignored-on-react-component

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
  • 5
    This is a nice and simple solution. It does however fire both the single click and double click when doing a double click, and so on for triple click. Not an issue for my current use case fortunately :) – DanV Apr 12 '21 at 15:46
  • 2
    If you only want double click you can check if `e.detail === 2`, doing so will filter single click and triple click @DanV – NearHuscarl Apr 12 '21 at 15:48
  • 2
    this is a great solution, much better than writing custom code to check a timer. The double-click delay can be customized in the user's OS, and the browser knows how to use that value. Your frontend code shouldn't hardcode the delay value. – andy Jun 29 '21 at 17:47
  • This one is simple and the best... – Paveloosha Jan 27 '22 at 09:53
  • 1
    TOP ANSWER! Works even if the element is re-rendered in response to the initial click event! I recommend this solution. My issue: the click selects the element; react re-renders the native element in order to display selection state in a large virtual grid; the second click is NOT a double-click as far as the newly created native element is concerned (on Chrome). But this method works even when doing heavy duty rendering and seems to be resistant to lag issues introduced by rendering time (timers are not). – Robin Davies Feb 28 '22 at 11:56
  • 1
    if(e.detail === 2){ console.log("####double click"); } – Shapon Pal Jul 18 '22 at 07:07
  • Does not seem to work on mobile view – duduwe Sep 09 '22 at 10:27
38

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>
  • 2
    Great solution, thank you! I'm just a bit puzzled: Is it actually necessary to use both "prevent" and "clearTimeout()"? Wouldn't either one be sufficient for this case? – Nicole Feb 26 '19 at 19:17
30

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>
Erminea Nea
  • 409
  • 4
  • 4
  • 3
    This is definitely the correct answer for year 2020 :) – Mustafa Yilmaz Dec 07 '20 at 19:46
  • 3
    Great solution, to make it perfect I would suggest to rename it into useSingleAndDoubleClick thanks ;) – Felipe May 01 '21 at 10:36
  • 1
    This is the perfect solution for my scenario as well. However I'm having trouble stoping the propagation on `callbackClick` and `callbackDoubleClick` . So, how can I pass the event to access in the `callbackDoubleClick` and `callbackClick` functions ? – Thidasa Pankaja Aug 03 '21 at 09:53
  • 1
    @ThidasaPankaja Did you find any way to pass the event to both the functions from this common function – Glory Raj Nov 11 '21 at 00:01
  • with event passage support https://codesandbox.io/s/usesingledoubleclick-hook-esimf – ThayalanGR Feb 01 '22 at 07:41
  • i have unfortunately found an issue in this answer. the callbackClick is kinda locking till the timer finishes. it's very unnatural TBH – darth pixel Sep 08 '22 at 05:13
19

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.

1. ref callback

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;

2. lodash debounce

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;

on the soapbox

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.

Jeff Fairley
  • 8,071
  • 7
  • 46
  • 55
  • How is it not an issue in React 15.3? – Sassan Feb 09 '17 at 10:45
  • 3
    @Sassan I'm not seeing this issue on the latest version of react (15.6.1) either -- both fire as expected. Note that on double click, the normal click handler fires twice as well but I'm pretty sure that's to be expected. – Greg Venech Jul 13 '17 at 21:16
4

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>
   </>
  )
 }
}
Alex Mireles
  • 121
  • 1
  • 7
  • Personally I don't like this solution because `handleDoubleClick` must be aware of what `handleClick` is doing to work properly. So if you change code of `handleClick` you should also adjust `handleDoubleClick`. – Hakier Mar 18 '23 at 21:29
2

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>
Blee
  • 419
  • 5
  • 11
0

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>
)
Manos
  • 1
  • 1
0

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)
    }
  }
DivineCoder
  • 702
  • 5
  • 11
0

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>
papaiatis
  • 4,231
  • 4
  • 26
  • 38
0

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>
Stanislau Buzunko
  • 1,630
  • 1
  • 15
  • 27
0

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

AliN11
  • 2,387
  • 1
  • 25
  • 40
-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;
jrswgtr
  • 2,287
  • 8
  • 23
  • 49
  • 1) are you sure it is reacting precisely to a double click and not a single click? i am not. 2) what does `List` has to do with this code? – Nik O'Lai Sep 24 '22 at 13:10