clauderic / dnd-kit

The modern, lightweight, performant, accessible and extensible drag & drop toolkit for React.
http://dndkit.com
MIT License
12.59k stars 627 forks source link

active.data.current gets lost "on drag end". #794

Open growthwp opened 2 years ago

growthwp commented 2 years ago

I have a draggable (also using an DragOverlay) defined as such:

  const { attributes, listeners, setNodeRef } = useDraggable({
    id: `${type}_${id}`,
    data: {
      id,
      type,
    },
  });

and I handle the onDragStart and onDragEnd where my DnDContext is rendering. If I console log active.data.current inside onDragStart, I can see the data I provided. Unfortunately, once I let go of the mouse and the drop should happen, active.data.current is just an empty object.

Using the latest version, I have no idea how to even replicate this bug or why this would ever happen. Any ideas?


Digging a little deeper, I see: https://github.com/clauderic/dnd-kit/blob/919f21af9382b88141b6305769836960d5844fa5/packages/core/src/components/DndContext/DndContext.tsx#L165 - "// It's possible for the active node to unmount while dragging". This is the only thing I see that would mess up with the data object and, it's true, the container unmounts if I drag outside of it.

Is there any way to fix this gracefully or do I have to "suspend" my state somehow such that the item remains mounted until I finish my drag?

wiltsu commented 2 years ago

Same here.

shawnshuang commented 2 years ago

I'm also experiencing active.data.current getting lost, but in the collisionDetection function. However, it seems to me that onDragStart and collisionDetection use the same active object (let me know if I'm wrong).

The version of @dnd-kit/core that I'm using is 6.0.3.

Update: I just came across this. I'll do some more digging and create a separate issue if necessary!

growthwp commented 2 years ago

@wiltsu @shawnshuang Assuming it is not a bug, as I noted, the only way for active.data.current to be missing is if the original draggable gets unmounted.

If you have something complex, you can just run a console.log on unmount to see if your draggable's really unmounting:

useEffect(() => {
  return () => {
    console.log('Unmounted.');
  }
}
Fabianopb commented 2 years ago

I'm experiencing the same issue within a virtualized list when dragging the item far enough (meaning that the virtualization unmounts the original instance).

It seems a bug to me as the data information should be preserved on the active object, just like the id is preserved.

RemyMachado commented 1 year ago

The issue is still open, but by any chance, have you found a workaround?

marcus-wishes commented 1 year ago

I use a React context to keep track on the tracked entity. useDraggable exposes isDragging. If this is true, I set the props of the component I want to drag in the context.

The DraggableOverlay renders the same (or similar) component as representation for the dragged one, if active from useDndContext is truthy, and this component uses the data of the context for its props. If active is falsy, I don't render anything, but one could also set the context value to null.

An example context could look like that (might have typos, I am in a hurry):

import React, { createContext, useContext, useEffect, useState } from "react"
import type { PropsType } from "../component"

type DraggedPropsContextType = {
    draggedProps: PropsType | null
    setDraggedProps: ( props: PropsType | null ) => void
}

const DraggedPropsContext = createContext<DraggedPropsContextType | undefined>( undefined )

export function useDraggedProps () {
    const context = useContext( DraggedPropsContext )
    if ( !context ) {
        throw new Error( "useDraggedProps must be used within a DraggedPropsProvider" )
    }
    return context
}

export function DraggedPropsProvider ( { children }: { children: React.ReactNode } ) {
    const [ draggedProps, setDraggedProps ] = useState<PropsType | null>( null )

    return (
        <DraggedPropsContext.Provider value={ { draggedProps, setDraggedProps } }>
            { children }
        </DraggedPropsContext.Provider>
    )
}

And the DraggableOverlay could be done like:

export default function DraggableSupport ( { children }: {children: React.ReactNode}) {
    return (
        <DraggedPropsProvider>
            <DndContext collisionDetection={ pointerWithin }>
                { children }
                <DraggableOverlayInner />
            </DndContext>
        </DraggedPropsProvider>
    )
}

function DraggableOverlayInner () {
    const { active } = useDndContext();
    return React.createPortal(
        <DragOverlay>
            { active && (
                <DraggableOverlayComponent />
            ) }
        </DragOverlay>,
        document.body
    );
}

function DraggableOverlayComponent () {
    const { draggedProps } = useDraggedProps() // from the DraggedProps context
    return (
            <>
                { draggedProps && <DraggableComponent { ...draggedProps } /> }
            </>
    )
}
josevizzn commented 2 months ago

Is there any update on this? I have the same issue

Redmega commented 1 month ago

I'm seeing this happen as well, in my case it seemed to be because a useDraggable and useDroppable both had an id of 0. Previously the useDroppable id of 0 worked, but when I happened to have a useDraggable id of 0 this draggable was missing active.data.current in the drop event.

I was able to fix by prefixing my ids, so i had draggable-0 and droppable-0 for example. This made it work. Whether the problem was having a useDraggable with an id of 0 (truthiness somewhere?) or because the id of the draggable and droppable were the same, I'm not sure. I haven't had the time to look deeper.