157

I want to make a draggable (that is, repositionable by mouse) React component, which seems to necessarily involve global state and scattered event handlers. I can do it the dirty way, with a global variable in my JS file, and could probably even wrap it in a nice closure interface, but I want to know if there's a way that meshes with React better.

Also, since I've never done this in raw JavaScript before, I'd like to see how an expert does it, to make sure I've got all the corner cases handled, especially as they relate to React.

Thanks.

Andrew Fleenor
  • 1,787
  • 2
  • 13
  • 12
  • Actually, I'd be at least as happy with a prose explanation as code, or even just, "you're doing it fine". But here's a JSFiddle of my work so far: http://jsfiddle.net/Z2JtM/ – Andrew Fleenor Jan 04 '14 at 21:32
  • I agree that this is a valid question, given that there are very few examples of react code to look at currently – Jared Forsyth Jan 04 '14 at 22:46
  • 1
    Found a simple HTML5 solution for my use case - https://youtu.be/z2nHLfiiKBA. Might help someone!! – Prem Sep 26 '17 at 17:22
  • Try this. It's a simple HOC to turn elements wrapped to be draggable https://www.npmjs.com/package/just-drag – Shan Feb 13 '20 at 09:26
  • In answer to a similar question I created a draggable reactjs app using hooks instead of classes to drag an image. the design focuses on touchscreens, where you can't use onDragStart or onDragEnd but the onTouchStart and onTouchEnd events. The touch events don't drag the icon automatically, you must implement it using onTouchMove. It works for android, iphone and surface book windows 10. [link](https://stackoverflow.com/questions/27837500/drag-and-drop-with-touch-support-for-react-js/71609160#71609160) – Julio Spinelli Mar 25 '22 at 22:28

15 Answers15

144

I should probably turn this into a blog post, but here's a pretty solid example.

The comments should explain things pretty well, but let me know if you have questions.

And here's the fiddle to play with: https://jsfiddle.net/Af9Jt/2/

var Draggable = React.createClass({
  getDefaultProps: function () {
    return {
      // allow the initial position to be passed in as a prop
      initialPos: {x: 0, y: 0}
    }
  },
  getInitialState: function () {
    return {
      pos: this.props.initialPos,
      dragging: false,
      rel: null // position relative to the cursor
    }
  },
  // we could get away with not having this (and just having the listeners on
  // our div), but then the experience would be possibly be janky. If there's
  // anything w/ a higher z-index that gets in the way, then you're toast,
  // etc.
  componentDidUpdate: function (props, state) {
    if (this.state.dragging && !state.dragging) {
      document.addEventListener('mousemove', this.onMouseMove)
      document.addEventListener('mouseup', this.onMouseUp)
    } else if (!this.state.dragging && state.dragging) {
      document.removeEventListener('mousemove', this.onMouseMove)
      document.removeEventListener('mouseup', this.onMouseUp)
    }
  },

  // calculate relative position of the mouse and set dragging=true
  onMouseDown: function (e) {
    // only left mouse button
    if (e.button !== 0) return
    var pos = $(this.getDOMNode()).offset()
    this.setState({
      dragging: true,
      rel: {
        x: e.pageX - pos.left,
        y: e.pageY - pos.top
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseUp: function (e) {
    this.setState({dragging: false})
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseMove: function (e) {
    if (!this.state.dragging) return
    this.setState({
      pos: {
        x: e.pageX - this.state.rel.x,
        y: e.pageY - this.state.rel.y
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  render: function () {
    // transferPropsTo will merge style & other props passed into our
    // component to also be on the child DIV.
    return this.transferPropsTo(React.DOM.div({
      onMouseDown: this.onMouseDown,
      style: {
        left: this.state.pos.x + 'px',
        top: this.state.pos.y + 'px'
      }
    }, this.props.children))
  }
})

Thoughts on state ownership, etc.

"Who should own what state" is an important question to answer, right from the start. In the case of a "draggable" component, I could see a few different scenarios.

Scenario 1

The parent should own the current position of the draggable. In this case, the draggable would still own its own "am I being dragged" state, but would call this.props.onChange(x, y) whenever a mousemove event occurs.

Scenario 2

The parent only needs to own the "non-moving position", and so the draggable would own it's "dragging position" but onmouseup would call this.props.onChange(x, y) and defer the final decision to the parent. If the parent doesn't like where the draggable ended up, it would reject the state update, and the draggable would "snap back" to its initial position.

Mixin or component?

@ssorallen pointed out that because draggable is more of an attribute than a thing in itself, it might serve better as a mixin. My experience with mixins is limited, so I haven't seen how they might help or get in the way in complicated situations. This might very well be the best option.

Community
  • 1
  • 1
Jared Forsyth
  • 12,808
  • 7
  • 45
  • 54
  • If (as I suspect after reading [Thinking in React](http://facebook.github.io/react/blog/2013/11/05/thinking-in-react.html)) the position-state really belongs to the parent of the draggable objects, would it be appropriate to put the dragging state in the parent and pass the changing position down through props? Would you recommend also passing a `mouseDown(component, event)` callback to the draggables or some other communication method? – Andrew Fleenor Jan 05 '14 at 06:26
  • 4
    Great example. This seems more appropriate as a [`Mixin`](http://facebook.github.io/react/docs/reusable-components.html#mixins) than a full class since "Draggable" isn't actually an object, it's an ability of an object. – Ross Allen Jan 06 '14 at 03:11
  • those are both good points. Depending on your application, the position might well "belong" to the parent. but the "am I dragging" state probably doesn't. Just like in the elemnt, the value belongs to the parent, but the "focus" state is local. So I would pass in a onChange(x, y) prop to the draggable. – Jared Forsyth Jan 06 '14 at 23:09
  • Scenario one for me; the position has to propagate to other components in real time. A Mixin does sound attractive. – Andrew Fleenor Jan 08 '14 at 00:21
  • @JaredForsyth what about little bit advanced scenario with draggable and droppable? Or sortable or all three for the matter of completeness? – vittore Feb 24 '14 at 21:05
  • 1
    @vittore Hmm sounds like that would make a good blog post. Or library. "react-ui" or something, to mirror jquery-ui? – Jared Forsyth Feb 25 '14 at 05:04
  • @JaredForsyth well reusable library for drag-drop-sortable thing as jquery-ui has , would be ideal case. Plus it seems to me that with react.js approach it is going to be more straight forward to use. – vittore Feb 25 '14 at 16:01
  • @vittore Definitely. And I think a solid community-drive UI library is something react really needs right now. – Jared Forsyth Feb 28 '14 at 20:41
  • what would happen if the draggable unit is dragged outside its parent? would it stop listening to these events? – s_curry_s Apr 01 '14 at 04:56
  • 2
    I played around with it a little bit, it seems like dragging outside its parent doesnt do anything, but all sorts of weird things happen when its dragged into another react component – s_curry_s Apr 01 '14 at 06:58
  • 1
    If it were a mixin, how would you override the render method in the inheriting component? – bennlich May 19 '14 at 21:16
  • @bennlich You wouldn't define render in the mixin. This is fine because all you need to do in the render method is add a handler for "onMouseDown" for this to work. – Matt Wonlaw Aug 02 '14 at 22:41
  • 12
    You can remove the jquery dependency by doing: `var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) };` If you're using jquery with react you're probably doing something wrong ;) If you need some jquery plugin I find it is usually easer and less code to re-write it in pure react. – Matt Wonlaw Aug 02 '14 at 22:43
  • 8
    Just wanted to follow up on the comment above by @MattCrinklaw-Vogt to say that a more bullet-proof solution is to use `this.getDOMNode().getBoundingClientRect()` - getComputedStyle can output any valid CSS property including `auto` in which case the code above will result in a `NaN`. See the MDN article: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect – Andru Mar 25 '15 at 22:47
  • 2
    And to follow up re `this.getDOMNode()`, that's been deprecated. Use a ref to get the dom node. https://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute – Chris Sattinger Mar 25 '16 at 14:49
  • I can't seem to figure out where transferPropsTo is coming from? – Ikeem Wilson Dec 20 '19 at 17:14
  • @JaredForsyth why would the experience be janky without adding and removing the event listeners in componentDidUpdate? – handris Jul 24 '20 at 05:44
  • Mixing React with jQuery isn't recommended – Hylle Aug 22 '20 at 05:03
  • 1
    @IkeemWilson very old react, nowadays you just apply props via spread syntax. – riv Nov 28 '20 at 18:19
  • Thank you for posting this. Do you know if there's a way to detect elements below the element you're dragging? (The solution in vanilla js doesn't work with React). – Maiya Jun 02 '21 at 14:47
  • If anyone is looking, I converted the above code to a modern react solution with axis stickiness https://codesandbox.io/s/draggable-component-react-mglg6d?file=/src/Draggable.js – Pankaj Apr 06 '23 at 07:27
72

I implemented react-dnd, a flexible HTML5 drag-and-drop mixin for React with full DOM control.

Existing drag-and-drop libraries didn't fit my use case so I wrote my own. It's similar to the code we've been running for about a year on Stampsy.com, but rewritten to take advantage of React and Flux.

Key requirements I had:

  • Emit zero DOM or CSS of its own, leaving it to the consuming components;
  • Impose as little structure as possible on consuming components;
  • Use HTML5 drag and drop as primary backend but make it possible to add different backends in the future;
  • Like original HTML5 API, emphasize dragging data and not just “draggable views”;
  • Hide HTML5 API quirks from the consuming code;
  • Different components may be “drag sources” or “drop targets” for different kinds of data;
  • Allow one component to contain several drag sources and drop targets when needed;
  • Make it easy for drop targets to change their appearance if compatible data is being dragged or hovered;
  • Make it easy to use images for drag thumbnails instead of element screenshots, circumventing browser quirks.

If these sound familiar to you, read on.

Usage

Simple Drag Source

First, declare types of data that can be dragged.

These are used to check “compatibility” of drag sources and drop targets:

// ItemTypes.js
module.exports = {
  BLOCK: 'block',
  IMAGE: 'image'
};

(If you don't have multiple data types, this libary may not be for you.)

Then, let's make a very simple draggable component that, when dragged, represents IMAGE:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var Image = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    // Specify all supported types by calling registerType(type, { dragSource?, dropTarget? })
    registerType(ItemTypes.IMAGE, {

      // dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? }
      dragSource: {

        // beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? }
        beginDrag() {
          return {
            item: this.props.image
          };
        }
      }
    });
  },

  render() {

    // {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into
    // { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }.

    return (
      <img src={this.props.image.url}
           {...this.dragSourceFor(ItemTypes.IMAGE)} />
    );
  }
);

By specifying configureDragDrop, we tell DragDropMixin the drag-drop behavior of this component. Both draggable and droppable components use the same mixin.

Inside configureDragDrop, we need to call registerType for each of our custom ItemTypes that component supports. For example, there might be several representations of images in your app, and each would provide a dragSource for ItemTypes.IMAGE.

A dragSource is just an object specifying how the drag source works. You must implement beginDrag to return item that represents the data you're dragging and, optionally, a few options that adjust the dragging UI. You can optionally implement canDrag to forbid dragging, or endDrag(didDrop) to execute some logic when the drop has (or has not) occured. And you can share this logic between components by letting a shared mixin generate dragSource for them.

Finally, you must use {...this.dragSourceFor(itemType)} on some (one or more) elements in render to attach drag handlers. This means you can have several “drag handles” in one element, and they may even correspond to different item types. (If you're not familiar with JSX Spread Attributes syntax, check it out).

Simple Drop Target

Let's say we want ImageBlock to be a drop target for IMAGEs. It's pretty much the same, except that we need to give registerType a dropTarget implementation:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var ImageBlock = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    registerType(ItemTypes.IMAGE, {

      // dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? }
      dropTarget: {
        acceptDrop(image) {
          // Do something with image! for example,
          DocumentActionCreators.setImage(this.props.blockId, image);
        }
      }
    });
  },

  render() {

    // {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into
    // { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }.

    return (
      <div {...this.dropTargetFor(ItemTypes.IMAGE)}>
        {this.props.image &&
          <img src={this.props.image.url} />
        }
      </div>
    );
  }
);

Drag Source + Drop Target In One Component

Say we now want the user to be able to drag out an image out of ImageBlock. We just need to add appropriate dragSource to it and a few handlers:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var ImageBlock = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    registerType(ItemTypes.IMAGE, {

      // Add a drag source that only works when ImageBlock has an image:
      dragSource: {
        canDrag() {
          return !!this.props.image;
        },

        beginDrag() {
          return {
            item: this.props.image
          };
        }
      }

      dropTarget: {
        acceptDrop(image) {
          DocumentActionCreators.setImage(this.props.blockId, image);
        }
      }
    });
  },

  render() {

    return (
      <div {...this.dropTargetFor(ItemTypes.IMAGE)}>

        {/* Add {...this.dragSourceFor} handlers to a nested node */}
        {this.props.image &&
          <img src={this.props.image.url}
               {...this.dragSourceFor(ItemTypes.IMAGE)} />
        }
      </div>
    );
  }
);

What Else Is Possible?

I have not covered everything but it's possible to use this API in a few more ways:

  • Use getDragState(type) and getDropState(type) to learn if dragging is active and use it to toggle CSS classes or attributes;
  • Specify dragPreview to be Image to use images as drag placeholders (use ImagePreloaderMixin to load them);
  • Say, we want to make ImageBlocks reorderable. We only need them to implement dropTarget and dragSource for ItemTypes.BLOCK.
  • Suppose we add other kinds of blocks. We can reuse their reordering logic by placing it in a mixin.
  • dropTargetFor(...types) allows to specify several types at once, so one drop zone can catch many different types.
  • When you need more fine-grained control, most methods are passed drag event that caused them as the last parameter.

For up-to-date documentation and installation instructions, head to react-dnd repo on Github.

Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • 10
    What do drag and drop and mouse dragging have in common other than using a mouse? Your answer is not related to a question at all and clearly is a library advertisement. – polkovnikov.ph Aug 28 '16 at 15:58
  • 1
    updated links to official examples of freely draggable stuff with react-dnd: [basic](https://react-dnd.github.io/react-dnd/examples/drag-around/naive), [advanced](https://react-dnd.github.io/react-dnd/examples/drag-around/custom-drag-layer) – uryga Aug 15 '20 at 19:33
  • 2
    @DanAbramov I appreciate your effort to make the post, without it I probably wouldn't have been steared towards *react-dnd* so quickly. Like many of your products, it seems needlessly complicated **at first**, but put together in a primely professional manner (going through your documentation I usually learn a thing or two about software engineering...) I've read "those who don't know unix are doomed to reimplement it... poorly," and I feel something similar may hold true for many of your creations (including this one). – Nathan Chappell Sep 28 '20 at 12:43
49

The answer by @codewithfeeling is horribly wrong and lags your page! Here's a version of his code with issues fixed and annotated. This should be the most up to date hook-based answer here now.

import React, { useRef, useState, useEffect, useCallback } from "react";

/// throttle.ts
export const throttle = (f) => {
  let token = null,
    lastArgs = null;
  const invoke = () => {
    f(...lastArgs);
    token = null;
  };
  const result = (...args) => {
    lastArgs = args;
    if (!token) {
      token = requestAnimationFrame(invoke);
    }
  };
  result.cancel = () => token && cancelAnimationFrame(token);
  return result;
};

/// use-draggable.ts
const id = (x) => x;
// complex logic should be a hook, not a component
const useDraggable = ({ onDrag = id } = {}) => {
  // this state doesn't change often, so it's fine
  const [pressed, setPressed] = useState(false);

  // do not store position in useState! even if you useEffect on
  // it and update `transform` CSS property, React still rerenders
  // on every state change, and it LAGS
  const position = useRef({ x: 0, y: 0 });
  const ref = useRef();

  // we've moved the code into the hook, and it would be weird to
  // return `ref` and `handleMouseDown` to be set on the same element
  // why not just do the job on our own here and use a function-ref
  // to subscribe to `mousedown` too? it would go like this:
  const unsubscribe = useRef();
  const legacyRef = useCallback((elem) => {
    // in a production version of this code I'd use a
    // `useComposeRef` hook to compose function-ref and object-ref
    // into one ref, and then would return it. combining
    // hooks in this way by hand is error-prone

    // then I'd also split out the rest of this function into a
    // separate hook to be called like this:
    // const legacyRef = useDomEvent('mousedown');
    // const combinedRef = useCombinedRef(ref, legacyRef);
    // return [combinedRef, pressed];
    ref.current = elem;
    if (unsubscribe.current) {
      unsubscribe.current();
    }
    if (!elem) {
      return;
    }
    const handleMouseDown = (e) => {
      // don't forget to disable text selection during drag and drop
      // operations
      e.target.style.userSelect = "none";
      setPressed(true);
    };
    elem.addEventListener("mousedown", handleMouseDown);
    unsubscribe.current = () => {
      elem.removeEventListener("mousedown", handleMouseDown);
    };
  }, []);
  // useEffect(() => {
  //   return () => {
  //     // this shouldn't really happen if React properly calls
  //     // function-refs, but I'm not proficient enough to know
  //     // for sure, and you might get a memory leak out of it
  //     if (unsubscribe.current) {
  //       unsubscribe.current();
  //     }
  //   };
  // }, []);

  useEffect(() => {
    // why subscribe in a `useEffect`? because we want to subscribe
    // to mousemove only when pressed, otherwise it will lag even
    // when you're not dragging
    if (!pressed) {
      return;
    }

    // updating the page without any throttling is a bad idea
    // requestAnimationFrame-based throttle would probably be fine,
    // but be aware that naive implementation might make element
    // lag 1 frame behind cursor, and it will appear to be lagging
    // even at 60 FPS
    const handleMouseMove = throttle((event) => {
      // needed for TypeScript anyway
      if (!ref.current || !position.current) {
        return;
      }
      const pos = position.current;
      // it's important to save it into variable here,
      // otherwise we might capture reference to an element
      // that was long gone. not really sure what's correct
      // behavior for a case when you've been scrolling, and
      // the target element was replaced. probably some formulae
      // needed to handle that case. TODO
      const elem = ref.current;
      position.current = onDrag({
        x: pos.x + event.movementX,
        y: pos.y + event.movementY
      });
      elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
    });
    const handleMouseUp = (e) => {
      e.target.style.userSelect = "auto";
      setPressed(false);
    };
    // subscribe to mousemove and mouseup on document, otherwise you
    // can escape bounds of element while dragging and get stuck
    // dragging it forever
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      handleMouseMove.cancel();
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
    // if `onDrag` wasn't defined with `useCallback`, we'd have to
    // resubscribe to 2 DOM events here, not to say it would mess
    // with `throttle` and reset its internal timer
  }, [pressed, onDrag]);

  // actually it makes sense to return an array only when
  // you expect that on the caller side all of the fields
  // will be usually renamed
  return [legacyRef, pressed];

  // > seems the best of them all to me
  // this code doesn't look pretty anymore, huh?
};

/// example.ts
const quickAndDirtyStyle = {
  width: "200px",
  height: "200px",
  background: "#FF9900",
  color: "#FFFFFF",
  display: "flex",
  justifyContent: "center",
  alignItems: "center"
};

const DraggableComponent = () => {
  // handlers must be wrapped into `useCallback`. even though
  // resubscribing to `mousedown` on every tick is quite cheap
  // due to React's event system, `handleMouseDown` might be used
  // in `deps` argument of another hook, where it would really matter.
  // as you never know where return values of your hook might end up,
  // it's just generally a good idea to ALWAYS use `useCallback`

  // it's nice to have a way to at least prevent element from
  // getting dragged out of the page
  const handleDrag = useCallback(
    ({ x, y }) => ({
      x: Math.max(0, x),
      y: Math.max(0, y)
    }),
    []
  );

  const [ref, pressed] = useDraggable({
    onDrag: handleDrag
  });

  return (
    <div ref={ref} style={quickAndDirtyStyle}>
      <p>{pressed ? "Dragging..." : "Press to drag"}</p>
    </div>
  );
};

See this code live here, a version with improved positioning of cursor with constrained onDrag here and hardcore hook showcase here.

(Previously this answer was about pre-hook React, and told the answer by Jared Forsyth is horribly wrong. It doesn't matter the least now, but it's still in edit history of the answer.)

polkovnikov.ph
  • 6,256
  • 6
  • 44
  • 79
  • 2
    Thanks for this, this is definitely not the most performant solution but it follows the best practices of building applications today. – Spets Aug 31 '16 at 05:50
  • Since the original link is dead probably worth noting that the issue with props in getInitialState wasn't that it was particularly egregious to pass a default value into a component as a prop, as long as it is clear what is happening and the intent is simply to hand over an immutable value. The real antipattern is if you instantiate state with the same key as a property, or worse still, try to achieve some kind of synchronization of props and state. That way madness lies! – ryan j Nov 04 '16 at 16:02
  • 1
    @ryanj Nope, default values are evil, that's the problem. What's the proper action when props change? Should we reset state to the new default? Should we compare the new default value with an old default value to reset state to default only when default did change? There's no way to restrict user to use only a constant value, and nothing else. That's why it's an antipattern. Default values should be created explicitly via high-order components (i.e. for the whole class, not for an object), and never should be set via props. – polkovnikov.ph Nov 08 '16 at 12:35
  • 1
    I respectfully disagree - component state is an excellent place to store data that is specific to the UI of a component, that has no relevance to the app as a whole, for example. Without being able to potentially pass default values as props in some instances, the options for retrieving this data post-mount are limited and in many (most?) circumstances less desirable than the vagaries around a component potentially being passed a different someDefaultValue prop at a later date. Im not advocating it as best practice or anything of the sort, its simply not as harmful as you're suggesting imo – ryan j Nov 14 '16 at 13:28
  • @ryanj What you say perfectly makes sense in a small project. Yes, careful use would not lead to such problems in case we were not human. Props have semantics of a changing value. Average colleagues (and our future selves) tend to forget that we have special semantics for a given set of props. Given the type system is absent in JS, we get bugs that are hell hard to find. Jumping through the hoops with defaulting and FRP is worth it just to avoid such situations. – polkovnikov.ph Nov 16 '16 at 11:10
  • 2
    Very simple and elegant solution indeed. I'm happy to see that my take on it was kind of similar. I do have one question: you mention poor performance, what would you propose to achieve a similar feature with performance in mind ? – Guillaume M Jan 13 '17 at 21:57
  • 1
    Elegant work! This should be the selected answer in 2017 (even better if performance question is addressed) – craftsman Oct 27 '17 at 09:19
  • Just mentioned that neither original solution nor mine is correct, as it doesn't take `margin` on body in consideration. Correct solution would use `event.pageX - (box.left - body.left)` as described [here](https://stackoverflow.com/a/11396681/1872046). [Example](https://jsfiddle.net/8wLmqh92/) – polkovnikov.ph Feb 20 '19 at 00:07
  • Is there a reason you are manually setting the transform style property in the componentDidUpdate lifecycle hook rather than just putting it in the render method and letting the reconciliation algorithm do the work? It’s more idiomatic and surely just as fast. – Alexander Rafferty Sep 09 '19 at 16:23
  • @AlexanderRafferty Regarding "surely just as fast": it's not. Reconciliation physically cannot work as fast as attribute update. – polkovnikov.ph Sep 09 '19 at 17:52
  • 1
    Anyway we have hooks now, and I have to update an answer once again soon. – polkovnikov.ph Sep 09 '19 at 17:53
  • Dude your hook and code is nice but for gods sakes please add the types for it, especially since you made it for typescript and even the comments have them listed as .ts files. – 55 Cancri May 17 '22 at 19:29
  • @55Cancri I've removed the types, because the question is specifically about JS, and already forgot about it. Probably should put them back, because nobody should be writing in JS anymore. – polkovnikov.ph May 19 '22 at 06:37
  • @polkovnikov.ph This is not working correctly in Safari. – Ramesh Reddy Jan 19 '23 at 17:26
  • @RameshReddy Oof. No surprise. Safari is a new Internet Explorer, and nothing works there on the first try. Fortunately, I don't have a MacBook :) Please, tell me if you figure out what is different there. – polkovnikov.ph Jan 19 '23 at 21:40
  • @polkovnikov.ph I think event.movementX/event.movementY values are different in Safari – Ramesh Reddy Jan 21 '23 at 07:29
  • @RameshReddy Actually I asked several people to try it in Safari yesterday, and it worked. What version do you use? – polkovnikov.ph Jan 21 '23 at 11:21
26

Here's a simple modern approach to this with useState, useEffect and useRef in ES6.

import React, { useRef, useState, useEffect } from 'react'

const quickAndDirtyStyle = {
  width: "200px",
  height: "200px",
  background: "#FF9900",
  color: "#FFFFFF",
  display: "flex",
  justifyContent: "center",
  alignItems: "center"
}

const DraggableComponent = () => {
  const [pressed, setPressed] = useState(false)
  const [position, setPosition] = useState({x: 0, y: 0})
  const ref = useRef()

  // Monitor changes to position state and update DOM
  useEffect(() => {
    if (ref.current) {
      ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`
    }
  }, [position])

  // Update the current position if mouse is down
  const onMouseMove = (event) => {
    if (pressed) {
      setPosition({
        x: position.x + event.movementX,
        y: position.y + event.movementY
      })
    }
  }

  return (
    <div
      ref={ ref }
      style={ quickAndDirtyStyle }
      onMouseMove={ onMouseMove }
      onMouseDown={ () => setPressed(true) }
      onMouseUp={ () => setPressed(false) }>
      <p>{ pressed ? "Dragging..." : "Press to drag" }</p>
    </div>
  )
}

export default DraggableComponent
codewithfeeling
  • 6,236
  • 6
  • 41
  • 53
  • 3
    This seems to be the most up-to-date answer here. – codyThompson Sep 09 '20 at 00:21
  • 1
    Most up-to-date, but incorrect. `position` is stored as state, which make component render on every tick of `mousemove`. Worse, it does it without any throttling. – polkovnikov.ph Aug 01 '21 at 17:18
  • Also it's argueable whether `onMouseMove` should be wrapped in `useCallback`, and why the module has an `export default` instead of a named export. – polkovnikov.ph Aug 01 '21 at 17:21
  • Now that I've read it once more, subscribing to `onMouseMove` only after `onMouseDown` might be important for performance, because React has to convert browser's event object into a synthetic event object at every mousemove tick, even though it's immediately discarded by failing `if (pressed)` check. – polkovnikov.ph Aug 26 '21 at 13:42
  • @polkovnikov.ph, could you specify what you would do to improve this code as it seems the best of them all to me? – DonBergen Dec 19 '21 at 17:30
  • @DonBergen [Here's](https://codesandbox.io/s/divine-glade-4rw54?file=/src/App.js) a rough outline of what has to be done. – polkovnikov.ph Dec 19 '21 at 22:26
  • Uhh, there wasn't much to be done to make it kinda production ready, so I just finished it and posted as an edit to my answer. Now it's 2AM, and it's already Monday :/ – polkovnikov.ph Dec 19 '21 at 22:52
18

react-draggable is also easy to use. Github:

https://github.com/mzabriskie/react-draggable

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Draggable from 'react-draggable';

var App = React.createClass({
    render() {
        return (
            <div>
                <h1>Testing Draggable Windows!</h1>
                <Draggable handle="strong">
                    <div className="box no-cursor">
                        <strong className="cursor">Drag Here</strong>
                        <div>You must click my handle to drag me</div>
                    </div>
                </Draggable>
            </div>
        );
    }
});

ReactDOM.render(
    <App />, document.getElementById('content')
);

And my index.html:

<html>
    <head>
        <title>Testing Draggable Windows</title>
        <link rel="stylesheet" type="text/css" href="style.css" />
    </head>
    <body>
        <div id="content"></div>
        <script type="text/javascript" src="bundle.js" charset="utf-8"></script>    
    <script src="http://localhost:8080/webpack-dev-server.js"></script>
    </body>
</html>

You need their styles, which is short, or you don't get quite the expected behavior. I like the behavior more than some of the other possible choices, but there's also something called react-resizable-and-movable. I'm trying to get resize working with draggable, but no joy so far.

allesklarbeidir
  • 312
  • 3
  • 10
Joseph Larson
  • 8,530
  • 1
  • 19
  • 36
18

I've updated polkovnikov.ph solution to React 16 / ES6 with enhancements like touch handling and snapping to a grid which is what I need for a game. Snapping to a grid alleviates the performance issues.

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

class Draggable extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            relX: 0,
            relY: 0,
            x: props.x,
            y: props.y
        };
        this.gridX = props.gridX || 1;
        this.gridY = props.gridY || 1;
        this.onMouseDown = this.onMouseDown.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onMouseUp = this.onMouseUp.bind(this);
        this.onTouchStart = this.onTouchStart.bind(this);
        this.onTouchMove = this.onTouchMove.bind(this);
        this.onTouchEnd = this.onTouchEnd.bind(this);
    }

    static propTypes = {
        onMove: PropTypes.func,
        onStop: PropTypes.func,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        gridX: PropTypes.number,
        gridY: PropTypes.number
    }; 

    onStart(e) {
        const ref = ReactDOM.findDOMNode(this.handle);
        const body = document.body;
        const box = ref.getBoundingClientRect();
        this.setState({
            relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft),
            relY: e.pageY - (box.top + body.scrollTop - body.clientTop)
        });
    }

    onMove(e) {
        const x = Math.trunc((e.pageX - this.state.relX) / this.gridX) * this.gridX;
        const y = Math.trunc((e.pageY - this.state.relY) / this.gridY) * this.gridY;
        if (x !== this.state.x || y !== this.state.y) {
            this.setState({
                x,
                y
            });
            this.props.onMove && this.props.onMove(this.state.x, this.state.y);
        }        
    }

    onMouseDown(e) {
        if (e.button !== 0) return;
        this.onStart(e);
        document.addEventListener('mousemove', this.onMouseMove);
        document.addEventListener('mouseup', this.onMouseUp);
        e.preventDefault();
    }

    onMouseUp(e) {
        document.removeEventListener('mousemove', this.onMouseMove);
        document.removeEventListener('mouseup', this.onMouseUp);
        this.props.onStop && this.props.onStop(this.state.x, this.state.y);
        e.preventDefault();
    }

    onMouseMove(e) {
        this.onMove(e);
        e.preventDefault();
    }

    onTouchStart(e) {
        this.onStart(e.touches[0]);
        document.addEventListener('touchmove', this.onTouchMove, {passive: false});
        document.addEventListener('touchend', this.onTouchEnd, {passive: false});
        e.preventDefault();
    }

    onTouchMove(e) {
        this.onMove(e.touches[0]);
        e.preventDefault();
    }

    onTouchEnd(e) {
        document.removeEventListener('touchmove', this.onTouchMove);
        document.removeEventListener('touchend', this.onTouchEnd);
        this.props.onStop && this.props.onStop(this.state.x, this.state.y);
        e.preventDefault();
    }

    render() {
        return <div
            onMouseDown={this.onMouseDown}
            onTouchStart={this.onTouchStart}
            style={{
                position: 'absolute',
                left: this.state.x,
                top: this.state.y,
                touchAction: 'none'
            }}
            ref={(div) => { this.handle = div; }}
        >
            {this.props.children}
        </div>;
    }
}

export default Draggable;
anyhotcountry
  • 311
  • 2
  • 4
  • 1
    hi @anyhotcountry what you use ***gridX*** coefficient for? – Alexey Nikonov Feb 20 '19 at 10:13
  • 2
    @AlexNikonov it's the size of the (snap-to) grid in the x direction. It is recommended to have gridX and gridY > 1 to improve performance. – anyhotcountry Feb 21 '19 at 11:35
  • 1
    This worked quite well for me. On change I made in the onStart() function: calculating relX and relY I used e.clienX-this.props.x. This allowed me to place the draggable component inside a parent container rather than depending on the entire page being the drag area. I know it's a late comment but just wanted to say thanks. – Geoff Jul 06 '20 at 22:51
16

Here's a 2020 answer with a Hook:

function useDragging() {
  const [isDragging, setIsDragging] = useState(false);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const ref = useRef(null);

  function onMouseMove(e) {
    if (!isDragging) return;
    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseUp(e) {
    setIsDragging(false);
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseDown(e) {
    if (e.button !== 0) return;
    setIsDragging(true);

    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });

    e.stopPropagation();
    e.preventDefault();
  }

  // When the element mounts, attach an mousedown listener
  useEffect(() => {
    ref.current.addEventListener("mousedown", onMouseDown);

    return () => {
      ref.current.removeEventListener("mousedown", onMouseDown);
    };
  }, [ref.current]);

  // Everytime the isDragging state changes, assign or remove
  // the corresponding mousemove and mouseup handlers
  useEffect(() => {
    if (isDragging) {
      document.addEventListener("mouseup", onMouseUp);
      document.addEventListener("mousemove", onMouseMove);
    } else {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    }
    return () => {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    };
  }, [isDragging]);

  return [ref, pos.x, pos.y, isDragging];
}

Then a component that uses the hook:


function Draggable() {
  const [ref, x, y, isDragging] = useDragging();

  return (
    <div
      ref={ref}
      style={{
        position: "absolute",
        width: 50,
        height: 50,
        background: isDragging ? "blue" : "gray",
        left: x,
        top: y,
      }}
    ></div>
  );
}
Evan Conrad
  • 3,993
  • 4
  • 28
  • 46
  • How would you work with touch compatibility here? – Sidharth Jan 13 '21 at 20:28
  • Does not work with element being inside some container with offset – stoefln Feb 10 '21 at 12:54
  • 2
    I did exactly what you wrote, but I have a problem, when I click on the element it moves far away from the mouse and starts tracking the movement but within a different pin point – Abdul-Elah JS Apr 24 '21 at 23:10
  • @stoefln you gotta use other data to get coords for the offset. like your dragging elenent offset in relation to its parent element would be calculated like that - x = ref.current.offsetLeft; y = ref.current.offsetTop; – alexey.metelkin Aug 12 '21 at 13:45
  • 1
    @Abdul-ElahJS same issue as the one stoefln had here. And when you using the data you might wanna use marginLeft and marginTop to position the dragged element; – alexey.metelkin Aug 12 '21 at 13:48
  • This is a great snippet of code, thanks Evan! I modified/improved it in my implementation by moving by translate() instead of using absolute positioning - as well as adding touch support with touchstart, touchmove, touchend, touchleave, touchcancel. – Mattis Nov 23 '21 at 15:48
5

Here is a simple another React hooks solution without any third party libraries, based on codewithfeeling and Evan Conrad's solutions. https://stackoverflow.com/a/63887486/1309218 https://stackoverflow.com/a/61667523/1309218

import React, { useCallback, useRef, useState } from "react";
import styled, { css } from "styled-components/macro";

const Component: React.FC = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const elementRef = useRef<HTMLDivElement>(null);

  const onMouseDown = useCallback(
    (event) => {
      const onMouseMove = (event: MouseEvent) => {
        position.x += event.movementX;
        position.y += event.movementY;
        const element = elementRef.current;
        if (element) {
          element.style.transform = `translate(${position.x}px, ${position.y}px)`;
        }
        setPosition(position);
      };
      const onMouseUp = () => {
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
      };
      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    },
    [position, setPosition, elementRef]
  );

  return (
    <Container>
      <DraggableItem ref={elementRef} onMouseDown={onMouseDown}>
      </DraggableItem>
    </Container>
  );
};

const Container = styled.div`
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  overflow: hidden;
`;

const DraggableItem = styled.div`
  position: absolute;
  z-index: 1;
  left: 20px;
  top: 20px;
  width: 100px;
  height: 100px;
  background-color: green;
`;
Tsuneo Yoshioka
  • 7,504
  • 4
  • 36
  • 32
3

I've updated the class using refs as all the solutions I see on here have things that are no longer supported or will soon be depreciated like ReactDOM.findDOMNode. Can be parent to a child component or a group of children :)

import React, { Component } from 'react';

class Draggable extends Component {

    constructor(props) {
        super(props);
        this.myRef = React.createRef();
        this.state = {
            counter: this.props.counter,
            pos: this.props.initialPos,
            dragging: false,
            rel: null // position relative to the cursor
        };
    }

    /*  we could get away with not having this (and just having the listeners on
     our div), but then the experience would be possibly be janky. If there's
     anything w/ a higher z-index that gets in the way, then you're toast,
     etc.*/
    componentDidUpdate(props, state) {
        if (this.state.dragging && !state.dragging) {
            document.addEventListener('mousemove', this.onMouseMove);
            document.addEventListener('mouseup', this.onMouseUp);
        } else if (!this.state.dragging && state.dragging) {
            document.removeEventListener('mousemove', this.onMouseMove);
            document.removeEventListener('mouseup', this.onMouseUp);
        }
    }

    // calculate relative position to the mouse and set dragging=true
    onMouseDown = (e) => {
        if (e.button !== 0) return;
        let pos = { left: this.myRef.current.offsetLeft, top: this.myRef.current.offsetTop }
        this.setState({
            dragging: true,
            rel: {
                x: e.pageX - pos.left,
                y: e.pageY - pos.top
            }
        });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseUp = (e) => {
        this.setState({ dragging: false });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseMove = (e) => {
        if (!this.state.dragging) return;

        this.setState({
            pos: {
                x: e.pageX - this.state.rel.x,
                y: e.pageY - this.state.rel.y
            }
        });
        e.stopPropagation();
        e.preventDefault();
    }


    render() {
        return (
            <span ref={this.myRef} onMouseDown={this.onMouseDown} style={{ position: 'absolute', left: this.state.pos.x + 'px', top: this.state.pos.y + 'px' }}>
                {this.props.children}
            </span>
        )
    }
}

export default Draggable;

  • i got this error: TypeError: react__WEBPACK_IMPORTED_MODULE_5___default.a.createRef is not a function – aldo Sep 30 '22 at 03:40
2

I would like to add a 3rd Scenario

The moving position is not saved in any way. Think of it as a mouse movement - your cursor is not a React-component, right?

All you do, is to add a prop like "draggable" to your component and a stream of the dragging events that will manipulate the dom.

setXandY: function(event) {
    // DOM Manipulation of x and y on your node
},

componentDidMount: function() {
    if(this.props.draggable) {
        var node = this.getDOMNode();
        dragStream(node).onValue(this.setXandY);  //baconjs stream
    };
},

In this case, a DOM manipulation is an elegant thing (I never thought I'd say this)

Thomas Deutsch
  • 2,344
  • 2
  • 27
  • 36
1

There's already plenty of answers, but I'll throw in mine as well. The advantages of this answer is as follows:

  • Modern hook-based solution
  • Uses Typescript
  • Dynamically adds/removes events for added performance benefit
  • Reasonable encapsulation; i.e. Draggable's position is relative to its immediate parent
  • Reference to parent is calculated in the Draggable component. It does not need to be passed in.
  • Simple, intuitive CSS
  • Draggable position clearly and explicitly clamped to parent's dimensions

import {
  CSSProperties,
  useEffect,
  useRef,
  useState,
  MouseEvent as r_MouseEvent,
  MutableRefObject,
} from 'react';


interface PositionType {
  x: number,
  y: number,
}


interface MinMaxType {
  min: number,
  max: number,
}


interface Props {
  text: string,
  position: PositionType
  isDragging?: boolean,
  style?: CSSProperties,
}


const clamp = (num: number, min: number, max: number): number => Math.min(max, Math.max(min, num));


const Draggable = ({
  text,
  position,
  style = {},
}: Props) => {
  const [pos, setPos] = useState<PositionType>();
  const draggableRef = useRef<HTMLDivElement>();
  const [parent, setParent] = useState<HTMLElement | null>();
  const [xBounds, setXBounds] = useState<MinMaxType>({ min: 0, max: 0 });
  const [yBounds, setYBounds] = useState<MinMaxType>({ min: 0, max: 0 });

  useEffect(() => {
    const parentElement: HTMLDivElement = draggableRef?.current?.parentElement as HTMLDivElement;
    const parentWidth: number = parentElement?.offsetWidth as number;
    const parentHeight: number = parentElement?.offsetHeight as number;
    const parentLeft: number = parentElement?.offsetLeft as number;
    const parentTop: number = parentElement?.offsetTop as number;

    const draggableWidth: number = draggableRef?.current?.offsetWidth as number;
    const draggableHeight: number = draggableRef?.current?.offsetHeight as number;

    setParent(parentElement);

    setPos({
      x: parentLeft + position.x,
      y: parentTop + position.y
    });

    setXBounds({
      min: parentLeft,
      max: parentWidth + parentLeft - draggableWidth,
    });

    setYBounds({
      min: parentTop,
      max: parentHeight + parentTop - draggableHeight,
    });
  }, [draggableRef, setParent, setPos, setXBounds, setYBounds, position]);

  const mouseDownHandler = (e: r_MouseEvent) => {
    if (e.button !== 0) return // only left mouse button

    parent?.addEventListener('mousemove', mouseMoveHandler);
    parent?.addEventListener('mouseup', mouseUpHandler);
    parent?.addEventListener('mouseleave', mouseUpHandler);

    e.stopPropagation();
    e.preventDefault();
  };

  const mouseMoveHandler = (e: MouseEvent) => {
    setPos({
      x: clamp(e.pageX, xBounds?.min, xBounds?.max),
      y: clamp(e.pageY, yBounds?.min, yBounds?.max),
    });

    e.stopPropagation();
    e.preventDefault();
  };

  const mouseUpHandler = (e: MouseEvent) => {
    parent?.removeEventListener('mousemove', mouseMoveHandler);
    parent?.removeEventListener('mouseup', mouseUpHandler);

    e.stopPropagation();
    e.preventDefault();
  };

  const positionStyle = pos && { left: `${pos.x}px`, top: `${pos.y}px` };
  const draggableStyle = { ..._styles.draggable, ...positionStyle, ...style } as CSSProperties;

  return (
    <div ref = { draggableRef as MutableRefObject <HTMLDivElement> }
      style = { draggableStyle }
      onMouseDown = {mouseDownHandler}>
      { text }
    </div>
  );
}


const _styles = {
  draggable: {
    position: 'absolute',
    padding: '2px',
    border: '1px solid black',
    borderRadius: '5px',
  },
};


export default Draggable;
Bud Linville
  • 193
  • 1
  • 10
  • You have the same bug as in answer of codewithfeeling: `setPos` is a very expensive operation, and it's not even throttled. That `text` prop is usually called `children` in React, and those `children` are gonna be rendered and reconciled hundred times a second. Also please don't use `as` in TypeScript (you just masked bugs in your types), don't use `export default` and don't use `stopPropagation`. – polkovnikov.ph Jan 08 '23 at 17:02
0

Elaborating on Evan Conrad's answer (https://stackoverflow.com/a/63887486/1531141) I came to this Typescript approach:

import { RefObject, useEffect, useRef, useState } from "react";

export enum DraggingState {
    undefined = -1,
    starts = 0, 
    moves = 1,
    finished = 2
}

export default function useDragging() {
    const [state, setState] = useState(DraggingState.undefined);        
    const [point, setPoint] = useState({x: 0, y: 0});                   // point of cursor in relation to the element's parent
    const [elementOffset, setElementOffset] = useState({x: 0, y: 0});   // offset of element in relation to it's parent
    const [touchOffset, setTouchOffset] = useState({x: 0, y: 0});       // offset of mouse down point in relation to the element
    const ref = useRef() as RefObject<HTMLDivElement>;

// shows active state of dragging
const isDragging = () => {
    return (state === DraggingState.starts) || (state === DraggingState.moves);
}

function onMouseDown(e: MouseEvent) {
    const parentElement = ref.current?.offsetParent as HTMLElement;
    if (e.button !== 0 || !ref.current || !parentElement) return;
    
    // First entry to the flow. 
    // We save touchOffset value as parentElement's offset 
    // to calculate element's offset on the move. 
    setPoint({
        x: e.x - parentElement.offsetLeft,
        y: e.y - parentElement.offsetTop
    });
    setElementOffset({
        x: ref.current.offsetLeft,
        y: ref.current.offsetTop
    });
    setTouchOffset({
        x: e.x - parentElement.offsetLeft - ref.current.offsetLeft,
        y: e.y - parentElement.offsetTop - ref.current.offsetTop
    });

    setState(DraggingState.starts);
}

function onMouseMove(e: MouseEvent) {
    const parentElement = ref.current?.offsetParent as HTMLElement;
    if (!isDragging() || !ref.current || !parentElement) return;
    setState(DraggingState.moves);
    
    setPoint({
        x: e.x - parentElement.offsetLeft,
        y: e.y - parentElement.offsetTop
    });
    setElementOffset({
        x: e.x - touchOffset.x - parentElement.offsetLeft,
        y: e.y - touchOffset.y - parentElement.offsetTop
    });
}

function onMouseUp(e: MouseEvent) {
    // ends up the flow by setting the state 
    setState(DraggingState.finished);
}


function onClick(e: MouseEvent) {
    // that's a fix for touch pads that transfer touches to click, 
    // e.g "Tap to click" on macos. When enabled, on tap mouseDown is fired,
    // but mouseUp isn't. In this case we invoke mouseUp manually, to trigger 
    // finishing state; 
    setState(DraggingState.finished);
}

// When the element mounts, attach an mousedown listener
useEffect(() => {
    const element = ref.current;
    element?.addEventListener("mousedown", onMouseDown);
    
    return () => {
        element?.removeEventListener("mousedown", onMouseDown);
    };
}, [ref.current]);

// Everytime the state changes, assign or remove
// the corresponding mousemove, mouseup and click handlers
useEffect(() => {
    if (isDragging()) {
        document.addEventListener("mouseup", onMouseUp);
        document.addEventListener("mousemove", onMouseMove);
        document.addEventListener("click", onClick);
    } else {
        document.removeEventListener("mouseup", onMouseUp);
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("click", onClick);
    }
    return () => {
        document.removeEventListener("mouseup", onMouseUp);
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("click", onClick);
    };
}, [state]);

return {
    ref: ref,
    state: state,
    point: point,
    elementOffset: elementOffset,
    touchOffset: touchOffset
   }
}

also added onClick handler as on touchpads with tap to click option enabled both onClick and mouseDown happen on the same moment, but mouseUp never gets fired up to close up the gesture.

Also, this hook returns three pairs of coords - element offset to its parent, grab point inside the element and a point inside the element's parent. See the comments inside the code for details;

Used like this:

const dragging = useDragging();
const ref = dragging.ref;

const style: CSSProperties = {
    marginLeft: dragging.elementOffset.x,
    marginTop: dragging.elementOffset.y,
    border: "1px dashed red"
}

return (<div ref={ref} style={style}>
           {dragging.state === DraggingState.moves ? "is dragging" : "not dragging"}
        </div>)
alexey.metelkin
  • 1,309
  • 1
  • 11
  • 20
0

Here is a dragable div example (tested) using react functional

function Draggable() {
  const startX = 300;
  const startY = 200;
  const [pos, setPos] = useState({ left: startX , top: startY });
  const [isDragging, setDragging] = useState(false);
  const isDraggingRef = React.useRef(isDragging);
  const setDraggingState = (data) => {
    isDraggingRef.current = data;
    setDragging(data);
  };

  function onMouseDown(e) {
    setDraggingState(true);
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseMove(e) {
    if (isDraggingRef.current) {
      const rect = e.target.parentNode.getBoundingClientRect();
      let newLeft = e.pageX - rect.left - 20;
      let newTop = e.pageY - rect.top - 20;

      if (
        newLeft > 0 &&
        newTop > 0 &&
        newLeft < rect.width &&
        newTop < rect.height
      ) {
        setPos({
          left: newLeft,
          top: newTop,
        });
      } else setDraggingState(false);
    }
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseUp(e) {
    setDraggingState(false);
    e.stopPropagation();
    e.preventDefault();
  }

  useEffect(() => {
    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);
  }, []);

  useEffect(() => {
      console.log(pos)
  }, [pos]);
  return <div style={pos} className="draggableDiv" onMouseDown={onMouseDown}></div>;
}
0

This is the simplest component for making an element draggable. Insert any element you want to make draggable inside this component and it will work.

import { useEffect, useState } from "react";

const DragAndDrop = ({ children }) => {
  const [isDragging, setIsDragging] = useState(false);
  const [xTranslate, setXTranslate] = useState(0);
  const [yTranslate, setYTranslate] = useState(0);
  const [initialMousePosition, setInitialMousePosition] = useState({});
  const onMouseDown = ({ clientX, clientY }) => {
    setInitialMousePosition({ x: clientX, y: clientY });
    setIsDragging(true);
  };
  useEffect(() => {
    const onMouseMove = (e) => {
      setXTranslate(xTranslate + e.clientX - initialMousePosition.x);
      setYTranslate(yTranslate + e.clientY - initialMousePosition.y);
    };
    if (isDragging) {
      window.addEventListener("mousemove", onMouseMove);
    }
    return () => window.removeEventListener("mousemove", onMouseMove);
  }, [isDragging, initialMousePosition]);
  useEffect(() => {
    const onMouseUp = () => setIsDragging(false);
    window.addEventListener("mouseup", onMouseUp);
    return () => window.removeEventListener("mouseup", onMouseUp);
  }, []);
  return (
    <div
      style={{ transform: `translate(${xTranslate}px,${yTranslate}px)` }}
      onMouseDown={onMouseDown}
    >
      {" "}
      {children}
    </div>
  );
};

export default DragAndDrop;

For example like this:

//In your main file:
<DragAndDrop>
<div>I am draggable</div>
</DragAndDrop>
-1

Do Not Use React Component and useEffect Hook for implementing the functionality of Dragging Containers

Here is the React Class Based Component ES6 Version -->

import React from "react";
import $ from 'jquery';
import { useRef } from "react";

class Temp_Class extends React.Component{

    constructor(props){
        super(props);
        this.state = {
            pos: {x:0, y:0},
            dragging: false,
            rel: null
        };
        this.onMouseDown = this.onMouseDown.bind(this);

        this.onMouseMove = this.onMouseMove.bind(this);

        this.onMouseUp = this.onMouseUp.bind(this);
    }

    componentDidUpdate(props, state){
        // console.log("Dragging State is ",this.state)
        if (this.state.dragging && !state.dragging) {
            document.addEventListener('mousemove', this.onMouseMove)
            document.addEventListener('mouseup', this.onMouseUp)
        } else if (!this.state.dragging && state.dragging) {
            document.removeEventListener('mousemove', this.onMouseMove)
            document.removeEventListener('mouseup', this.onMouseUp)
        }
    }

    onMouseDown(e){
      console.log("Mouse Down")
      if (e.button !== 0) return
      var pos = document.getElementById("contianer").getBoundingClientRect();
    //   console.log(pos)
      this.setState({
        dragging: true,
        rel: {
          x: e.pageX - pos.left,
          y: e.pageY - pos.top
        }
      })
      e.stopPropagation()
      e.preventDefault()
    }

    onMouseUp(e) {
        console.log("Mouse Up")
        this.setState({dragging: false})
        e.stopPropagation()
        e.preventDefault()
    }    

    onMouseMove(e) {
        console.log("Mouse Move")
        if (!this.state.dragging) return
        this.setState({
          pos: {
            x: e.pageX - this.state.rel.x,
            y: e.pageY - this.state.rel.y
          }
        })
        e.stopPropagation()
        e.preventDefault()
      console.log("Current State is ", this.state)
      }

    render(){
        return (<div id="contianer" style = {{
            position: 'absolute',
            left: this.state.pos.x + 'px',
            top: this.state.pos.y + 'px',
            cursor: 'pointer',
            width: '200px',
            height: '200px',
            backgroundColor: '#cca',
        }} onMouseDown = {this.onMouseDown}>
            Lovepreet Singh
        </div>);
    }
}


export default Temp_Class;