Pierre Olivier Tran
Pierre Olivier Tran

Reputation: 1879

Delay React onMouseOver event

I have a list of elements, when hovering one of these, I'd like to change my state.

<ListElement onMouseOver={() => this.setState({data})}>Data</ListElement>

Unfortunately, if I move my mouse over the list, my state changes several times in a quick succession. I'd like to delay the change on state, so that it waits like half a second before being fired. Is there a way to do so?

Upvotes: 12

Views: 24007

Answers (5)

Filip Seman
Filip Seman

Reputation: 1714

debounce is always the answer if you want to limit the action in a time frame.

Implementation is simple, no need for external libraries.

implementation:

type Fnc = (...args: any[]) => void;

// default 300ms delay
export function debounce<F extends Fnc>(func: F, delay = 300) {
    type Args = F extends (...args: infer P) => void ? P : never;
    let timeout: any;
    return function (this: any, ...args: Args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

usage:

...

/** any action you want to debounce */
function foo(
    data: any, 
    event: React.MouseEvent<HTMLDivElement, MouseEvent>
): void {
    this.setState({data});
}

const fooDebounced = debounce(foo, 500);

<ListElement onMouseOver={fooDebounced.bind(null, data)}>
    Data
</ListElement>
...

You don't actually have to bind a function, but it's a good habit if you loop through multiple elements to avoid initializing a new function for each element.

Upvotes: 1

Adriel Tosi
Adriel Tosi

Reputation: 291

I might be a little late for this but I'd like to add to some of the answers above using Lodash debounce. When debouncing a method, the debounce function lets you cancel the call to your method based on some event. See example for functional component:

const [isHovered, setIsHovered] = React.useState(false)

const debouncedHandleMouseEnter = debounce(() => setIsHovered(true), 500)

const handlOnMouseLeave = () => {
  setIsHovered(false)
  debouncedHandleMouseEnter.cancel()
}

return (
  <div
    onMouseEnter={debouncedHandleMouseEnter}
    onMouseLeave={handlOnMouseLeave}
  >
   ... do something with isHovered state...
  </div>
)

This example lets you call your function only once the user is hovering in your element for 500ms, if the mouse leaves the element the call is canceled.

Upvotes: 14

Ian Smith
Ian Smith

Reputation: 209

Here's a way you can delay your event by 500ms using a combination of onMouseEnter, onMouseLeave, and setTimeout.

Keep in mind the state update for your data could be managed by a parent component and passed in as a prop.

import React, { useState } from 'react'

const ListElement = () => {
    const [data, setData] = useState(null)
    const [delayHandler, setDelayHandler] = useState(null)

    const handleMouseEnter = event => {
        setDelayHandler(setTimeout(() => {
            const yourData = // whatever your data is

            setData(yourData)
        }, 500))
    }

    const handleMouseLeave = () => {
        clearTimeout(delayHandler)
    }

    return (
        <div
            onMouseEnter={handleMouseEnter}
            onMouseLeave={handleMouseLeave}
        >
            I have a delayed event handler
        </div>
    )
}

export default ListElement

Upvotes: 20

Orelsanpls
Orelsanpls

Reputation: 23515

You can create a method that will trigger the onMouseOver event when matching special requirements.

In the further example, it triggers after 500 ms.

/**
 * Hold the descriptor to the setTimeout
 */
protected timeoutOnMouseOver = false;

/**
 * Method which is going to trigger the onMouseOver only once in Xms
 */
protected mouseOverTreatment(data) {
   // If they were already a programmed setTimeout
   // stop it, and run a new one
   if (this.timeoutOnMouseOver) {
     clearTimeout(this.timeoutOnMouseOver);
   }

   this.timeoutOnMouseOver = setTimeout(() => {
      this.setState(data);

      this.timeoutOnMouseOver = false;
   }, 500);
}

Upvotes: 2

hsz
hsz

Reputation: 152216

You can use debounce as a dedicated package or get it from lodash, etc:

Useful for implementing behavior that should only happen after a repeated action has completed.

const debounce = require('debounce');

class YourComponent extends Component {
  constructor(props) {
    super(props);

    this.debouncedMouseOver = debounce(handleMouseOver, 200);
  }

  handleMouseOver = data => this.setState({ data });

  render() {
    const data = [];
    return <ListElement onMouseOver={() => this.debouncedMouseOver(data)}>Data</ListElement>;
  }
}

Upvotes: 6

Related Questions