react-dnd / react-dnd

Drag and Drop for React
http://react-dnd.github.io/react-dnd
MIT License
20.93k stars 1.99k forks source link

After unmounting the source and mounting the target on drag start, it's impossible to trigger drag end without touching #3060

Open Sun-2 opened 3 years ago

Sun-2 commented 3 years ago

Expected behavior

The backend is react-dnd-touch-backend, the testing is done on an actual phone, and with Chrome's devtools.

I've got an application with react-dnd and react-router-dom. If the user starts dragging an item on route /drag, a history.push to /drop is issued, where the item can be dropped. Therefore, on drag start, the source is unmounted, and the target is mounted.

Since the issue is reproducible just with useState instead of react-router-dom, I've used it for simplicity. The idea is the same:

Actual behavior

Steps to reproduce

Versions

"react": "^17.0.1",

"react-dnd": "^11.1.3",
"react-dnd-touch-backend": "^11.1.3",

What browser are you using?

Chrome 88.0.4324.150 (Official Build) (64-bit) OS: Ubuntu 20.10

Demo

useDrag throws with nextCreate is not a function in CodeSandbox, therefore here's a GitHub repo. The link leads to the only relevant file.

wdjennings commented 3 years ago

Hi there @Sun-2 ! I saw your post and had a similar issue, thought I would post my solution in case it helps.

I was only removing the source item after the drag started. As with you, as soon as my source item is removed the touch would 'pause'. I would need to lift my finger and then tap once more to make the drag end event happen.

For me (and maybe for you!) this happened because touchmove events REQUIRE the original source item to stay mounted. See the answer in this post, and the associated links they add: https://stackoverflow.com/questions/56653453/why-touchmove-event-is-not-fired-after-dom-changes.

I solved the issue by 'caching' the source item in the list where it was being removed, a bit like this (shortened my code because there's lots of other logic going on in that component -- please ask if this is not clear):

const MyList = ({ items }) => {

  // items: (array of strings) identifying item ids in my list
  const removedItemRef = React.useRef(null);

  // Add cached removed item back into list
  // Despite it not being in 'items' passed to this component
  if (removedItemRef.current){
    if (!items.includes(removedItemRef.current)){
      items = [ ...items, removedItemRef.current ]
    }
  }

  const renderItem = (item, index) => {
    return (
      <DraggableItem
        itemId={item}
        handleDragStart={() => {
          // When dragging starts, cache the id
          removedItemRef.current = item;
        }}
        handleDragEnd={() => {
          // When dragging ends, clear cached id
          removedItemRef.current = null;
        }}
      />
    );
  }

  return (
    <div>
      {items.map(renderItem)}
    </div>
  );
}