0

I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:

Here is the class:

import React from "react"

class NavigationCloseMouseButton extends React.Component {
  static defaultProps = {
    visible: true,
    offsetX: 0,
    offsetY: 0,
  }

  state = {
    xPosition: 0,
    yPosition: 0,
    mouseMoved: false,
    listenerActive: false,
  }

  componentDidMount() {
    this.addListener()
  }

  componentDidUpdate() {
    this.updateListener()
  }

  componentWillUnmount() {
    this.removeListener()
  }

  getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
    this.setState({
      xPosition,
      yPosition,
      mouseMoved: true,
    })
  }

  addListener = () => {
    window.addEventListener("mousemove", this.getTooltipPosition)
    this.setState({ listenerActive: true })
  }

  removeListener = () => {
    window.removeEventListener("mousemove", this.getTooltipPosition)
    this.setState({ listenerActive: false })
  }

  updateListener = () => {
    if (!this.state.listenerActive && this.props.visible) {
      this.addListener()
    }

    if (this.state.listenerActive && !this.props.visible) {
      this.removeListener()
    }
  }

  render() {
    return (
      <div
        onClick={this.props.toggleNavigation}
        className="tooltip color-bg"
        style={{
          display:
            this.props.visible && this.state.mouseMoved ? "block" : "none",
          opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
          top: this.state.yPosition + this.props.offsetY,
          left: this.state.xPosition + this.props.offsetX,
        }}
      >
        Close Menu
      </div>
    )
  }
}

export default NavigationCloseMouseButton

And this is what I've so far, but results with errors: ReferenceError: getTooltipPosition is not defined


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

const NavigationCloseMouseButton = () => {
  const defaults = {
    visible: true,
    offsetX: 0,
    offsetY: 0,
  }

  const defaultState = {
    xPosition: 0,
    yPosition: 0,
    mouseMoved: false,
    listenerActive: false,
  }

  const [defaultProps, setDefaultProps] = useState(defaults)
  const [state, setState] = useState(defaultState)

  useEffect(() => {
    // Update the document title using the browser API
    addListener()
  }, [])

  getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
    setState({
      xPosition,
      yPosition,
      mouseMoved: true,
    })
  }

  addListener = () => {
    window.addEventListener("mousemove", getTooltipPosition)
    setState({ listenerActive: true })
  }

  removeListener = () => {
    window.removeEventListener("mousemove", getTooltipPosition)
    setState({ listenerActive: false })
  }

  updateListener = () => {
    if (!state.listenerActive && props.visible) {
      addListener()
    }

    if (state.listenerActive && !props.visible) {
      removeListener()
    }
  }

  return (
    <div
      onClick={props.toggleNavigation}
      className="tooltip color-bg"
      style={{
        display: props.visible && state.mouseMoved ? "block" : "none",
        opacity: props.visible && state.mouseMoved ? "1" : "0",
        top: state.yPosition + props.offsetY,
        left: state.xPosition + props.offsetX,
      }}
    >
      Close Menu
    </div>
  )
}

export default NavigationCloseMouseButton

Jan
  • 12,992
  • 9
  • 53
  • 89
  • 1
    Well...where did you declare `getTooltipPosition`? I see only an assignment. That's true also for the other functions. Note that with `useEffect({}, [])` you can return a function to perform what you did in `componentWillUnmount()`. Also: what `defaultProps` is for? Definitely they won't be seen as defaults for component's properties. Final note: when you call `setState({ x: y })` then you replace the ENTIRE previous value, it does not behave like `Component.setState()` where what you provide is automatically merged with the existing state. – Adriano Repetti Jul 29 '20 at 15:48

1 Answers1

0

Setting Defaults

You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.

const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {

Updating a Listener

I'm sure there a lots of great answers about this so I won't go into too much detail.

You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.

I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.

Calculating Offset

I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.

// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);

// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);

Animation

Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.

Code

const NavigationCloseMouseButton = ({
  visible = true,
  toggleNavigation
}) => {

  // state of the movement
  const [state, setState] = useState({
    xPosition: 0,
    yPosition: 0,
    mouseMoved: false
  });

  // memoized event listener
  const getTooltipPosition = useCallback(
    // plain event, not a React synthetic event
    ({ clientX: xPosition, clientY: yPosition }) => {
      setState({
        xPosition,
        yPosition,
        mouseMoved: true
      });
    },
    []
  ); // never re-creates

  useEffect(() => {
    // don't need to listen when it's not visible
    if (visible) {
      window.addEventListener("mousemove", getTooltipPosition);
    } else {
      window.removeEventListener("mousemove", getTooltipPosition);
    }

    // clean-up function to remove on unmount
    return () => {
      window.removeEventListener("mousemove", getTooltipPosition);
    };
  }, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes

  // ref to DOM node for measuring
  const divRef = useRef(null);

  // can caluculate offset instead of passing in props
  const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
  const offsetY = -.5 * (divRef.current?.offsetHeight || 0);

  // don't show until after mouse is moved
  const isVisible = visible && state.mouseMoved;

  return (
    <div
      ref={divRef}
      onClick={toggleNavigation}
      // control most styling through className
      className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
      style={{
        // need absolute position to use top and left
        position: "absolute",
        top: state.yPosition + offsetY,
        left: state.xPosition + offsetX
      }}
    >
      Close Menu
    </div>
  );
};

Other Uses

We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.

  • Get the contents from props.children instead of always using "Close Menu"
  • Accept a className as a prop
  • Change the name of toggleNavigation to onClick

Code Sandbox Demo

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102