Change width on mousedown and mousemove in React

Viewed 1380

I want to build a hook that changes the width of an element on mousemove and mousedown events.

I'm using the following code (that is actually working):

import React, { useState, useCallback, useEffect, useRef } from "https://cdn.skypack.dev/react";
import ReactDOM from "https://cdn.skypack.dev/react-dom";

const useMouseDelta = (initialWidth) => {
  const [dragging, setDragging] = useState(false);
  const [result, setResult] = useState(initialWidth);
  const origin = useRef(initialWidth);

  const handleMouseDown = useCallback((e) => {
    origin.current = e.clientX;
    setDragging(true);
  }, []);

  const handleMouseUp = useCallback((e) => {
    setDragging(false);
  }, []);

  const handleMouseMove = useCallback((e) => {
      if (!dragging) {
        return;
      }
      setResult(result + (e.clientX - origin.current));
    },
    [dragging]
  );

  useEffect(() => {
    window.addEventListener('mousedown', handleMouseDown);
    window.addEventListener('mouseup', handleMouseUp);
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousedown', handleMouseDown);
      window.removeEventListener('mouseup', handleMouseUp);
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, [dragging]);

  return result;
};


const Test = () => {
  const width = useMouseDelta(400);
  return (
    <div className="container" style={{width: width}}>
      <div className="resize"></div>
    </div> 
  )
}

ReactDOM.render(<Test />, document.getElementById('root'));

However, I'm seeing this warning in ESLint:

React Hook useCallback has a missing dependency: 'result'. Either include it or remove the dependency array. You can also do a functional update 'setResult(r => ...)' if you only need 'result' in the 'setResult' call

So I change it to:

setResult(r => r + (e.clientX - origin.current));

Now the drag is not working as desired anymore.

An example can be found here: CodePen example

2 Answers
  1. You don't need to store origin, but you need to store previousClientX with useRef() and add just a change of clientX between two events.
  2. You don't need to store dragging with useState(), useRef() is enough.
  3. You need to add all handlers to the dependencies list in your useEffect().

The improved code:

CodePen

import React, {
  useState,
  useCallback,
  useEffect,
  useRef
} from "https://cdn.skypack.dev/react";
import ReactDOM from "https://cdn.skypack.dev/react-dom";

const useMouseDelta = (initialWidth) => {
  const [result, setResult] = useState(initialWidth);
  const dragging = useRef(false);
  const previousClientX = useRef(0);

  const handleMouseMove = useCallback((e) => {
    if (!dragging.current) {
      return;
    }

    setResult((result) => {
      const change = e.clientX - previousClientX.current;
      previousClientX.current = e.clientX;
      return result + change;
    });
  }, []);

  const handleMouseDown = useCallback((e) => {
    previousClientX.current = e.clientX;
    dragging.current = true;
  }, []);

  const handleMouseUp = useCallback((e) => {
    dragging.current = false;
  }, []);

  useEffect(() => {
    window.addEventListener("mousedown", handleMouseDown);
    window.addEventListener("mouseup", handleMouseUp);
    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousedown", handleMouseDown);
      window.removeEventListener("mouseup", handleMouseUp);
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, [handleMouseDown, handleMouseUp, handleMouseMove]);

  return result;
};

const Test = () => {
  const width = useMouseDelta(400);

  return (
    <div className="container" style={{ width }}>
      <div className="resize" />
    </div>
  );
};

ReactDOM.render(<Test />, document.getElementById("root"));

In this Effect you can add all the dependencies like this:

useEffect(() => {
    window.addEventListener('mousedown', handleMouseDown);
    window.addEventListener('mouseup', handleMouseUp);
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousedown', handleMouseDown);
      window.removeEventListener('mouseup', handleMouseUp);
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, [dragging, handleMouseDown, handleMouseUp, handleMouseMove]);

In the handleMouseMove it's a bit more tricky. It's ommited on purpose, so that it will only see the 'currentWidth' of the component and not the updates versions. The setResult can be used like this:

  const handleMouseMove = useCallback((e) => {
      if (!dragging) {
        return;
      }
      setResult((result) => result + (e.clientX - origin.current));
    },
    [dragging]
  );

Then the warning will not come up, but you will see that it will always see the updated value and it will cause extra additions.

A solution would be to store the current witdth in a different state like this in useMouseDelta and then it can be added as a dependency:

const useMouseDelta = (initialWidth: number) => {
  // The distance the mouse has moved since `mousedown`.
  const [dragging, setDragging] = useState(false);
  const [result, setResult] = useState(initialWidth);
  const [currentWidth, setCurrentWidth] = useState(initialWidth);

  // Original position independent of any dragging.  Updated when done dragging.
  const origin = useRef(initialWidth);

  const handleMouseDown = useCallback((e) => {
    origin.current = e.clientX;
    setDragging(true);
  }, []);

  const handleMouseUp = useCallback(
    (e) => {
      setDragging(false);
      setCurrentWidth(result);
    },
    [result]
  );

  const handleMouseMove = useCallback(
    (e) => {
      if (!dragging) {
        return;
      }
      setResult(() => currentWidth + e.clientX - origin.current);
    },
    [currentWidth, dragging]
  );

  useEffect(() => {
    window.addEventListener("mousedown", handleMouseDown);
    window.addEventListener("mouseup", handleMouseUp);
    window.addEventListener("mousemove", handleMouseMove);
    return () => {
      window.removeEventListener("mousedown", handleMouseDown);
      window.removeEventListener("mouseup", handleMouseUp);
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, [dragging, handleMouseDown, handleMouseUp, handleMouseMove]);

  return result;
};
Related