thisisnotabus
thisisnotabus

Reputation: 2069

onClick works but onDoubleClick is ignored on React component

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

Answers (16)

Arjen
Arjen

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

benshabatnoam
benshabatnoam

Reputation: 7630

2024 Update

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

PhrozenByte
PhrozenByte

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

AliN11
AliN11

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

Blee
Blee

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

sumanta
sumanta

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

Stanislau Buzunko
Stanislau Buzunko

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

papaiatis
papaiatis

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

Erminea Nea
Erminea Nea

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

NearHuscarl
NearHuscarl

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 

Live Demo

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

Upvotes: 75

DivineCoder
DivineCoder

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

Manos
Manos

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

Alex Mireles
Alex Mireles

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

Akanksha Chaturvedi
Akanksha Chaturvedi

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

Jeff Fairley
Jeff Fairley

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.

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.

Upvotes: 19

Ross Allen
Ross Allen

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

Related Questions