call addEventListener inside addEventListener callback function in react

Viewed 45

I am a student studying react. There's a problem with the component I'm making right now. What I am trying to make is a scroll-type picker similar to the link below.
https://codepen.io/gnauhca/pen/JrdpZZ
In the process of implementing this, there is a problem in registering and removing eventListener in eventListener.

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

function Scroller() {
  const array = [170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180];
  const [dragEventHistory, setDragEventHistory] = useState([]);
  const [dragEventStart, setDragEventStart] = useState(null);

  const scrollerContainer = useRef();
  const scrollerList = useRef();

  useEffect(() => {
    const moveList = (clientY) => {
      const difference = Number(dragEventStart) - Number(clientY);
      scrollerList.current.style.transform = `translate3d(0, -${difference}px, 0)`;
    };

    const onMouseMoveEventHandler = (e) => {
      console.log(e);
      e.preventDefault();
      moveList(e.clientY);
    };

    const onMouseDownEventHandler = (e) => {
      console.log(e);
      e.preventDefault();
      setDragEventStart(e.clientY);
      scrollerContainer.current.addEventListener(
        "mousemove",
        onMouseMoveEventHandler
      );
    };

    const onMouseUpEventHandler = (e) => {
      console.log(e);
      e.preventDefault();
      scrollerContainer.current.removeEventListener(
        "mousemove",
        onMouseMoveEventHandler
      );
    };

    scrollerContainer.current.addEventListener(
      "mousedown",
      onMouseDownEventHandler
    );
    scrollerContainer.current.addEventListener(
      "mouseup",
      onMouseUpEventHandler
    );

    return () => {
      scrollerContainer.current.removeEventListener(
        "mousedown",
        onMouseDownEventHandler
      );
      scrollerContainer.current.removeEventListener(
        "mouseup",
        onMouseUpEventHandler
      );
    };
  }, []);

  return (
    <div
      className="scroller-container"
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        overflow: "hidden",
        width: 200,
        height: 250,
      }}
      ref={scrollerContainer}
      // onTouchStart={onTouchStartEventHandler}
      // onTouchEnd={onTouchEndEventHandler}
    >
      <ul className="scroller-list" ref={scrollerList}>
        {array.map((item, index) => {
          return (
            <li
              key={index}
              style={{
                width: 200,
                height: 50,
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                backgroundColor: "orange",
                fontSize: 30,
              }}
            >
              {item}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

export default Scroller;

If I define functions without using useEffect, depending on the change of 'dragEventStart' state, the callback function registered as an event cannot be removed in the process of component rerendering.
If I define functions in useEffect, removeEventListener function can point the callback function that is same with addEventListener function's callback. But dragEventStart state can't be changed. However, if dragEventStart is included in the dependency of useEffect, it becomes the same as when useEffect is not used.

How can I solve this problem? I need help!!

1 Answers

Instead of declaring dragEventStart a React state variable and relying on proper enclosures of values, I suggest converting it to a React ref that can be mutated at will.

Also, instead of continually adding/removing event listeners just add the mouse down|up|move listeners once when the component mounts, and remove them in the returned useEffect hook cleanup function.

To make the UX a bit better for the user the mouse up and move event listeners should be attached to the window object so drags don't get "stuck" if the user isn't over the scrollerContainer element when releasing the mouse button.

Example:

function Scroller() {
  ...
  const [dragEventHistory, setDragEventHistory] = useState([]);
  const dragEventStartRef = useRef(null);

  const scrollerContainer = useRef();
  const scrollerList = useRef();

  useEffect(() => {
    const moveList = (clientY) => {
      // Only handle move if drag is started
      if (dragEventStartRef.current) {
        const difference = Number(dragEventStartRef.current) - Number(clientY);
        scrollerList.current.style.transform = `translate3d(0, -${difference}px, 0)`;
      }
    };

    const onMouseMoveEventHandler = (e) => {
      e.preventDefault();
      moveList(e.clientY);
    };

    const onMouseDownEventHandler = (e) => {
      e.preventDefault();
      dragEventStartRef.current = e.clientY;
    };

    const onMouseUpEventHandler = (e) => {
      e.preventDefault();
      dragEventStartRef.current = null;
    };

    scrollerContainer.current.addEventListener(
      "mousedown",
      onMouseDownEventHandler
    );
    window.addEventListener("mouseup", onMouseUpEventHandler);
    window.addEventListener("mousemove", onMouseMoveEventHandler);

    return () => {
      scrollerContainer.current.removeEventListener(
        "mousedown",
        onMouseDownEventHandler
      );
      window.removeEventListener("mouseup", onMouseUpEventHandler);
      window.removeEventListener("mousemove", onMouseMoveEventHandler);
    };
  }, []);

  return (
    ...
  );
}
Related