Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active October 4, 2021 11:21
Show Gist options
  • Save gragland/cfc4089e2f5d98dde5033adc44da53f8 to your computer and use it in GitHub Desktop.
Save gragland/cfc4089e2f5d98dde5033adc44da53f8 to your computer and use it in GitHub Desktop.
import { useRef, useState, useEffect } from 'react';
// Usage
function App() {
const [hoverRef, isHovered] = useHover();
return (
<div ref={hoverRef}>
{isHovered ? '😁' : '☹️'}
</div>
);
}
// Hook
function useHover() {
const [value, setValue] = useState(false);
const ref = useRef(null);
const handleMouseOver = () => setValue(true);
const handleMouseOut = () => setValue(false);
useEffect(
() => {
const node = ref.current;
if (node) {
node.addEventListener('mouseover', handleMouseOver);
node.addEventListener('mouseout', handleMouseOut);
return () => {
node.removeEventListener('mouseover', handleMouseOver);
node.removeEventListener('mouseout', handleMouseOut);
};
}
},
[ref.current] // Recall only if ref changes
);
return [ref, value];
}
@butchler
Copy link

butchler commented Jun 5, 2019

Here is another version that fixes the issue by using a callback ref, but does not use useRefEffect: https://codesandbox.io/s/usehover-ue8v3

@gragland
Copy link
Author

@butchler Thanks for pointing out this issue and sharing some fixes! I still see some value in sharing the existing version, since it's easier to understand and I'm guessing most people won't be changing the element the ref gets passed to, but I'll update the post description to link out to this alternate version: https://gist.github.com/gragland/a32d08580b7e0604ff02cb069826ca2f (same as yours, with some extra commenting).

@butchler
Copy link

@gragland Thank you :)

@jcready
Copy link

jcready commented Sep 10, 2019

Binding event listeners to the node can lead to the hover state getting out of sync if you move your mouse quickly. Instead bind the listeners to the document and check to see if the event target is our node or our node contains the event target:

import { useRef, useState, useEffect } from "react";

export default function useHover() {
  const [value, setValue] = useState(false);

  const ref = useRef(null);

  const handleMouseOver = e => {
    const node = ref.current;
    if (!node) return setValue(false);
    setValue(e.target === node || node.contains(e.target));
  };

  useEffect(
    () => {
      const node = ref.current;
      if (node) {
        const doc = node.ownerDocument;
        doc.addEventListener("mouseover", handleMouseOver);

        return () => {
          doc.removeEventListener("mouseover", handleMouseOver);
        };
      }
    },
    [ref.current] // Recall only if ref changes
  );

  return [ref, value];
}

@gragland
Copy link
Author

@jcready What do you mean by getting out of sync? Your example seems reasonable, just want to understand what the issue with the current implementation is.

@mbelsky
Copy link

mbelsky commented Feb 19, 2020

Hey @gragland
Current implementation re-creates handleMouseOver and handleMouseOut callbacks on every state update. There is a fix for that: https://gist.github.com/mbelsky/909c7a6b9bde3289e91a6448ae1a74b3/revisions#diff-0dd251e6c939d6c6f3846a366eade1f2

@ivanstnsk
Copy link

ivanstnsk commented May 24, 2020

Hello everyone!
I've made a Typescript version:

import { useEffect, useState, useRef } from 'react';


type THook<T extends HTMLElement> = [
  React.RefObject<T>,
  boolean,
];

export const useMouseHover = <T extends HTMLElement>(): THook<T> => {
  const [hovered, setHovered] = useState(false);
  const ref = useRef<T>(null);

  useEffect(() => {
    const handleMouseOver = (): void => setHovered(true);
    const handleMouseOut = (): void => setHovered(false);
    const node = ref && ref.current;

    if (node) {
      node.addEventListener('mouseover', handleMouseOver);
      node.addEventListener('mouseout', handleMouseOut);
      return () => {
        node.removeEventListener('mouseover', handleMouseOver);
        node.removeEventListener('mouseout', handleMouseOut);
      };
    }
  }, [ref]);

  return [ref, hovered];
};

Example of usage:

const [buttonRef, buttonHovered] = useMouseHover<HTMLButtonElement>();
const color = buttonHovered ? 'red' : 'blue';

return (
  <button
    ref={buttonRef}
    style={{ color }}
  >
    click me
  </button>
);

@forresto
Copy link

forresto commented Oct 8, 2020

React Hook React.useEffect has an unnecessary dependency: 'ref.current'. Either exclude it or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component. eslint (react-hooks/exhaustive-deps)

    [ref.current] // Recall only if ref changes

could be changed to

    [ref] // Recall only if ref changes

?

@mbelsky
Copy link

mbelsky commented Oct 8, 2020

@forresto this change won't fix that issue. Try @jjenzz's solution

@GlynL
Copy link

GlynL commented Dec 4, 2020

I had an issue with the last event being fired being a mouseover event. I switched the mouseover to mouseenter and mouseout to mouseleave which solved it. Any drawbacks to this method? It also limits the amount of events firing as it doesn't fire on child elements.

@Theo-flux
Copy link

I don't really think you need the useRef hook.
you could just use the useState hook and the onMouseEnter and onMouseLeave as props on the component.

what do you think?

@Theo-flux
Copy link

import React,{useState,useRef,useEffect} from "react"

export default function Image({className,image}){
    const [ishover, setIsHover] = useState(false)
    console.log(ishover)  
    
    return(
        <div 
            className={`${className} image-container`}
            onMouseEnter={() => setIsHover(true)}
            onMouseLeave={() => setIsHover(false)}
        >
            <img src={image.url} className="image-grid"/>    
        </div>
    )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment