react-dnd: Connecting monitor.getClientOffset to a DropTarget does not refresh props

I created a DropTarget and connected getClientOffset as follows:

targetCollect = function (connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
    clientOffset: monitor.getClientOffset()
  }
}

However, when moving drag within the component, the target does not receive updated props with each wiggle of the mouse (only on enter and exit). The target spec’s hover is called however repeatedly. Is there a reason that the clientOffset prop is not injected each time it is changed?

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 14
  • Comments: 22 (3 by maintainers)

Most upvoted comments

+1. Running into this and wasting a lot of time trying to understand why getClientOffset() works fine in canDrop() - which does get called repeatedly… - but no way to forward to my control ‘where’ over the item we are. I need to be able to insert above and below a treenode, not just “on” it. canDrop() has no way that I can find that lets me actually tell my component which of the many “types” of drops the node has so that my control can redisplay accordingly. Changing the prop in canDrop resulted in an error. Subscribing to onMouseMove doesn’t work during drag and drop operations. This has been a huge waste of time so far…any help would be appreciated.

@ibash, would you mind posting a gist of what you did to solve your problem?

Btw. one other really ugly hack around this issue can be to send in the coordinates as state from dropTarget.hover():

const dropTarget = {
    hover(props, monitor, component) {
        // HACK! Since there is an open bug in react-dnd, making it impossible
        // to get the current client offset through the collect function as the
        // user moves the mouse, we do this awful hack and set the state (!!)
        // on the component from here outside the component.
        component.setState({
            currentDragOffset: monitor.getClientOffset(),
        });
    },
    drop() { /* ... */ },
};

Of course there are tons of more correct ways to do this that would avoid abusing of setState like that. But at least this little hack is very condensed and may do the job until this issue is eventually fixed. It lets you hack around the bug without changing other components and/or files and without relying on library internals.

Edit: this is basically the same as noah79 does, I just didn’t read his code before now.

I was also running into this today and wasted some hours. What about leaving a note about it in the documentation under DropTarget -> The Collecting Function? That would at least spare some others for frustrations.

Here’s a gist of how I solved this. The trick is to just reach into the react-dnd internals and use the global monitor directly. Take a look the DragDropMonitor.js source to get an idea of what methods are available. ref: https://github.com/gaearon/dnd-core/blob/master/src/DragDropMonitor.js

You’ll have to excuse the coffeescript 😃

  # in the component being dragged, get access to the dragDropManager by adding
  # it to the contextTypes
  #
  # dragDropManager is an instance of this:
  # https://github.com/gaearon/dnd-core/blob/master/src/DragDropManager.js

  @contextTypes: {
    dragDropManager: React.PropTypes.object
  }

  # because we can receive events after the component is unmounted, we need to
  # keep track of whether the component is mounted manually.
  #
  # @_monitor is what lets us access all the nice internal state - it is an instance of this:
  # https://github.com/gaearon/dnd-core/blob/master/src/DragDropMonitor.js

  componentWillMount: () =>
    @_isMounted = true
    @_monitor = @context.dragDropManager.getMonitor()

    @_unsubscribeToStateChange = @_monitor.subscribeToStateChange(@_onStateChange)
    @_unsubscribeToOffsetChange = @_monitor.subscribeToOffsetChange(@_onOffsetChange)

  componentWillUnmount: () =>
    @_isMounted = false
    @_monitor = null

    @_unsubscribeToStateChange()
    @_unsubscribeToOffsetChange()

  # we can access dragging / dropping state by accessing the monitor 

  _onStateChange: () =>
    return unless @_isMounted

    # When we stop dragging reset the counter state and hide all cursors.
    if @_previousIsDragging && !@_monitor.isDragging()
      console.log('no longer dragging')
    @_previousIsDragging = @_monitor.isDragging()

  _onOffsetChange: () =>
    return unless @_isMounted

    # mouse is the x/y coordinates
    mouse = @_monitor.getClientOffset()

    # item is the drag item
    item = @_monitor.getItem()

    # if you want to check if a dragged item is over a target, you need the
    # targetId -- in the DropTarget wrapper you can pass it in like:
    #
    #   (connect, monitor) ->
    #     {
    #       targetId: monitor.targetId,
    #       connectDropTarget: connect.dropTarget()
    #     }
    #
    # and then you can use it like below

    @_monitor.isOverTarget(@props.targetId))

Does this make sense? If not I can add more details

Oh, good point. That’s right, I didn’t anticipate this usage. Currently, the only way to opt into tracking client offset is to use DragLayer. Otherwise, for performance reasons, it only updates the props if something concerning the drop target itself has changed.

I’d say it’s an API problem, and there may be several solutions:

  • Forbid reaching into getClientOffset() and similar methods from inside collect function and tell to use DragLayer instead (easy but dumb);
  • Automatically figure out that user wants to track the offset and subscribe to the offset changes if this is the case (harder but more consistent).

I think I’d accept a PR doing the second option. You can check DragLayer implementation to see how it subscribes to the offset changes.

Is there any plan to support this? I have an idea for a hack-free workaround: make a wrapper for DropTarget that also wraps hover to trigger another call to the collecting function and a re-render if the collecting function returns new values.

From my tests, react-dnd is consuming the mouse move events as soon as an item is being dragged over the target. If it were possible for the html-backend not to consume this event then the target component could just place a normal mousemove listener on its dom conditionally when the isOver property is set to true. Thereafter this listener could pick up the mouse position (in the normal way) when something is being dragged over it. I got this to work temporarily using setState from the drag() method react is giving warnings about setting state in the middle of a render transition.

I also ran into this issue. What are your thoughts on letting clients pass a flag into DropTarget spec to subscribe to offset changes? It’s one more “gotcha” for the user, but is simpler than manually subscribing/unsubscribing to offsets.