1

I'm using @use-gesture/react to drag a div around. The dragging is working just fine, but it keeps "bumping into" something. It appears that the motion is constrained by some containing div, but I can't for the life of me figure out why.

There are a few things I can imagine would be useful in diagnosing this, and I don't know how to do any of them :)

  • is there some event that fires when the component "runs into" something, such that I can log to console what it's hitting?
  • is there any way to sort of visually inspect the DOM along the z-index? (I have tried manually scouring the DOM, and can't see anything that would cause a problem)
  • is there some way to watch local variables in the react-developer-tools chrome extension?

I'm in no way constrained to the above — I'll take any and all advice on how to figure this out. More details, below.

Details

At a high level, I'm trying to add a moderately complicated component that has some draggable sub-components to part of a larger app. So, something like

const App = () => {
...
return (
<BrowserRouter >
  ... lots of code
    <SomeComplicatedThing /> // with some sub-component that uses @use-gesture
  ...

Everything is very close to working, except for this annoying problem of the drag action appearing to run into some boundary.

After going over both the code and the DOM seemingly countless times and not being able to spot any reason for the problem, I decided to manually reconstruct one page to try and reproduce the problem and narrow it down to some specific component.

You can see that I'm using useDrag with the hook bound to a div. When the event fires, I put a copy of the div in a portal tied to the body, and then update the top and left style attributes inside of a useLayoutEffect block:

function App() {
  function useColumnDragState() {
    const [dragState, setDragState] = useState(null);

    const bindDrag = useDrag(({ first, last, event, xy }) => {
      if (first && event) {
        event.preventDefault();

        setDragState({
          ... // do state stuff
        });
      } else if (last) {
        setDragState(null);
      }
      ...
    }, {});

    return {
      dragState,
      dragEventBinder: bindDrag
    };
  }

  const {
    dragState,
    dragEventBinder
  } = useColumnDragState();
  const eventHandlers = useMemo(() => dragEventBinder(), [dragEventBinder]);

  const ColumnDragObject = ({ dragState }) => {
    const referenceBoxRef = useRef(null);
    const dragBoxRef = useRef(null);

    const dragObjectPortal = dragState ? createPortal(
      <div> // wrapper
        <div ref={dragBoxRef}> // object positioner
          <div> // object holder
            <MyComponent /> //clone
          </div>
        </div>
      </div>,
      document.body
    )
      : null;

    // set up initial position
    ...

    // subscribe to live position updates without state changes
    useLayoutEffect(() => {
      if (dragState) {
        dragState.updateListeners['dragObj'] = (xy: number[]) => {
          if (!dragBoxRef.current) {
            return;
          }

          dragBoxRef.current.style.left = `${xy[0]}px`;
          dragBoxRef.current.style.top = `${xy[1]}px`;
        };
      }
    }, [dragState]);

    return <div ref={referenceBoxRef}>{dragObjectPortal}</div>
  }

  return (
    <>
     ...
       ... // many layers of nested stuff
         ...
             <div {...eventHandlers}> // draggable
               <MyComponent /> // original
     ...
        <ColumnDragObject dragState={dragState}/>
     ...
    </>
  );
}

export default App;

As I said, that's a much simplified version of both the component and the app, extracted out into one file. Much to my annoyance, it works just fine (i.e., it doesn't demonstrate the problem).

Next I did what maybe should have been my first step, and pulled <SomeComplicatedThing /> out into a stand-alone app with it at the top level:

import React, { Fragment } from 'react';
import { CssBaseline } from '@mui/material';
import SomeComplicatedThing from './components/SomeComplicatedThing';

function App() {
  return (
    <>
      <CssBaseline enableColorScheme />
      <div className="App">
        <SomeComplicatedThing />
      </div>
    </>
  );
}

export default App;

As I said, I probably should have done that first, because it does exhibit the problem. So, I opened the two up side-by-side, and stepped through the DOM. As far as I can see in the DOM between the working one (where I can drag stuff all over the page), and the not working one (where the drag appears to be constrained to some enclosing element) there is no difference in the hierarchy, and no difference in display, position, overflow, or z-index for any of the elements in the hierarchy.

So, how do I debug this from here? As I said in the beginning, my initial thoughts were

  • is there some event that fires when the component "runs into" something, such that I can log to console what it's hitting?

I realise there's no actual running into things. The actual behaviour is that the pointer can move anywhere on the screen, but the <div> whose top and left attributes are being updated in the above code stops moving beyond a certain point (updates are still happening — if you move the pointer back from the "out of bounds" portion of the screen, the <div> moves again). One thought I have is that maybe some containing element has some attribute set, such that it precludes the xy coordinates from being updated, and there may be some event that fires when that "fail to update" occurs that can tell me which element is doing the blocking.

  • is there any way to sort of visually inspect the DOM along the z-index? (I have tried manually scouring the DOM, and can't see anything that would cause a problem)

I keep reading sort of parenthetically in digging around about this that there can sometimes be z-index issues with react-use-gesture. I had thought going the createPortal route would avoid any of those; nevertheless, it would be good to get some visual 3-D representation of the DOM and make sure I'm not missing something "obvious"

  • is there some way to watch local variables in the react-developer-tools chrome extension?

As a last resort, I tried looking at what xy coordinates were being set, thinking that I might see something to make me go "aha!" when it runs into whatever is containing it. However, if I try and set a watch on xy naively in react-developer-tools, it doesn't work. In order to set a watch on it, I need to set a breakpoint in the enclosing function, and then set the watch. The problem is, since we're updating mouse position, every time you move at all, it triggers the break point, but if I remove the break point, it stops watching the variable. So ... how can I get it to dynamically watch a variable local to a function without setting a breakpoint?

And, of course, as I said at the beginning, any other debugging ideas are most definitely welcome!

Background

Given that I have the "simpler" version working, and that the normal course of development is to build-test-build-test-build-test ... until one gets to a finished product, it's natural to wonder how I got to the "complicated and broken" state in the first place. The answer is, my starting point is react-csv-importer. However, there are two aspects of that package which are rather explicitly the opposite of what I want: it's written in TypeScript and the UI theme is standalone. I'm removing the TypeScript parts to make it pure JavaScript, and making the UI be MUI.

philolegein
  • 1,099
  • 10
  • 28

0 Answers0