javascript - Anbefalt måte å gjøre React-komponent/div draggable


Jeg vil lage en dragbar (det vil si, flyttbar med mus) React-komponent, som tilsynelatende nødvendigvis involverer global tilstand og spredte hendelsesbehandlere. Jeg kan gjøre det på en skitten måte, med en global variabel i JS-filen min, og kan sannsynligvis til og med pakke det inn i et fint lukkegrensesnitt, men jeg vil vite om det finnes en måte som passer bedre med React.

Også, siden jeg aldri har gjort dette i ren JavaScript før, vil jeg gjerne se hvordan en ekspert gjør det, for å sikre at jeg har håndtert alle hjørnetilfeller, spesielt når det gjelder React.

Takk.

Svar #1

Jeg burde sannsynligvis gjøre dette om til et blogginnlegg, men her er et ganske solid eksempel.

Kommentarene bør forklare ting ganske godt, men gi meg beskjed hvis du har spørsmål.

Og her er fiolinen å leke med: 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))
  }
})

Tankar om statseierskap, osv.

«Hvem skal eie hvilken tilstand» er et viktig spørsmål å besvare, helt fra starten. I tilfelle av en «draggable»-komponent, kunne jeg se for meg noen forskjellige scenarier.

Scenario 1

Forelderen bør eie den nåværende posisjonen til det som kan dras. I dette tilfellet ville det som kan dras fortsatt eie sin egen "blir jeg dratt"-tilstand, men ville kalle this.props.onChange(x, y) hver gang en mousemove-hendelse skjer.

Scenario 2

Forelderen trenger bare å eie «ikke-bevegende posisjon», og dermed vil den dragbare eie sin «dra-posisjon», men onmouseup vil kalle this.props.onChange(x, y) og overlate den endelige beslutningen til forelderen. Hvis forelderen ikke liker hvor den dragbare endte opp, vil den avvise statusoppdateringen, og den dragbare vil «sprette tilbake» til sin opprinnelige posisjon.

Mixin eller komponent?

@ssorallen påpekte at fordi draggable er mer et attributt enn en ting i seg selv, kan det fungere bedre som en mixin. Min erfaring med mixins er begrenset, så jeg har ikke sett hvordan de kan hjelpe eller komme i veien i kompliserte situasjoner. Dette kan veldig godt være det beste alternativet.

Svar #2

Jeg implementerte react-dnd, en fleksibel HTML5 drag-and-drop-mixin for React med full DOM-kontroll.

Eksisterende dra-og-slipp-biblioteker passet ikke til mitt brukstilfelle, så jeg skrev mitt eget. Det ligner på koden vi har kjørt i omtrent ett år på Stampsy.com, men omskrevet for å utnytte React og Flux.

Nøkkelkravene jeg hadde:

  • Sender ut null DOM eller CSS på egen hånd, og overlater det til de konsumerende komponentene;
  • Pålegg så lite struktur som mulig på konsumerende komponenter;
  • Bruk HTML5 dra og slipp som primær backend, men gjør det mulig å legge til forskjellige backender i fremtiden;
  • Som den originale HTML5 API-en, legg vekt på å dra data og ikke bare «draggable views»;
  • Skjul HTML5 API-quirks for koden som bruker dem;
  • Ulike komponenter kan være «dra-kilder» eller «slipp-mål» for ulike typer data;
  • Tillat at en komponent kan inneholde flere dra-kilder og slipp-mål når det er nødvendig;
  • Gjør det enkelt for droppmål å endre utseendet hvis kompatible data blir dratt eller svevd over;
  • Gjør det enkelt å bruke bilder for dra-miniaturbilder i stedet for elementskjermbilder, og omgå nettleserens rariteter.

Hvis disse høres kjent ut for deg, les videre.

Bruk

Enkel dra-kilde

Først, deklarer typer av data som kan dras.

Disse brukes til å sjekke «kompatibilitet» mellom dra-kilder og slipp-mål:

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

(Hvis du ikke har flere datatyper, kan det hende at dette biblioteket ikke er for deg.)

La oss da lage en veldig enkel dra-og-slipp-komponent som, når den dras, representerer 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)} />
    );
  }
);

Ved å spesifisere configureDragDrop, forteller vi DragDropMixin om dra-og-slipp-oppførselen til denne komponenten. Både dra- og slippbare komponenter bruker den samme mixin-en.

Inne i configureDragDrop må vi kalle registerType for hver av våre tilpassede ItemTypes som komponenten støtter. For eksempel kan det være flere representasjoner av bilder i appen din, og hver av dem vil gi en dragSource for ItemTypes.IMAGE.

Et dragSource er bare et objekt som spesifiserer hvordan dra-kilden fungerer. Du må implementere beginDrag for å returnere elementet som representerer dataene du drar, og eventuelt noen få alternativer som justerer dra-grensesnittet. Du kan valgfritt implementere canDrag for å forby draing, eller endDrag(didDrop) for å utføre noe logikk når slippet har (eller ikke har) skjedd. Og du kan dele denne logikken mellom komponenter ved å la en delt mixin generere dragSource for dem.

Til slutt må du bruke {...this.dragSourceFor(itemType)} på noen (ett eller flere) elementer i render for å koble til dra-håndtak. Dette betyr at du kan ha flere «drag handles» i ett element, og de kan til og med tilsvare ulike elementtyper. (Hvis du ikke er kjent med JSX Spread Attributes-syntaksen, sjekk det ut).

Enkel slippmål

La oss si at vi vil at ImageBlock skal være et slippmål for IMAGEs. Det er stort sett det samme, bortsett fra at vi må gi registerType en dropTarget-implementering:

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>
    );
  }
);

Dra kilde + slipp mål i én komponent

La oss si vi nå vil at brukeren skal kunne dra ut et bilde fra ImageBlock. Vi trenger bare å legge til passende dragSource til det og noen få håndterere:

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>
    );
  }
);

Hva annet er mulig?

Jeg har ikke dekket alt, men det er mulig å bruke dette API-et på noen flere måter:

  • Bruk getDragState(type) og getDropState(type) for å finne ut om draing er aktiv, og bruk det til å bytte CSS-klasser eller attributter;
  • Spesifiser dragPreview som Image for å bruke bilder som dra-platsholdere (bruk ImagePreloaderMixin for å laste dem);
  • Si, vi vil gjøre ImageBlocks omorganiserbare. Vi trenger bare at de implementerer dropTarget og dragSource for ItemTypes.BLOCK.
  • Anta at vi legger til andre typer blokker. Vi kan gjenbruke deres omordningslogikk ved å plassere den i en mixin.
  • dropTargetFor(...types) lar deg spesifisere flere typer samtidig, slik at én sleppe-sone kan fange mange forskjellige typer.
  • Når du trenger mer detaljert kontroll, blir de fleste metodene gitt drag-hendelsen som forårsaket dem som den siste parameteren.

For oppdatert dokumentasjon og installasjonsinstruksjoner, gå til react-dnd-repositoriet på Github.

Svar #3

Svaret fra @codewithfeeling er fryktelig galt og gjør at siden din henger! Her er en versjon av koden hans med problemene fikset og kommentert. Dette bør nå være det mest oppdaterte svaret basert på hooks her.

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>
  );
};

Se denne koden live her, en versjon med forbedret posisjonering av markøren med begrenset onDrag her og en hardcore hook-visning her.

(Tidligere handlet dette svaret om pre-hook React, og sa at svaret av Jared Forsyth er fryktelig feil. Det spiller ingen rolle lenger, men det er fortsatt i redigeringshistorikken til svaret.)

Svar #4

Her er en enkel moderne tilnærming til dette med useState, useEffect og useRef i 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

Svar #5

Jeg har oppdatert polkovnikov.ph-løsningen til React 16/ES6 med forbedringer som berøringshåndtering og snapping til et rutenett, noe som er det jeg trenger for et spill. Snapping til et rutenett lindrer ytelsesproblemene.

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;

Svar #6

react-draggable er også lett å bruke. 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')
);

Og min 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>

Du trenger deres stiler, som er korte, ellers får du ikke helt den forventede oppførselen. Jeg liker oppførselen bedre enn noen av de andre mulige valgene, men det finnes også noe som heter react-resizable-and-movable. Jeg prøver å få resizing til å fungere med draggable, men ingen suksess så langt.

Svar #7

Her er et svar fra 2020 med en krok:

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];
}

Deretter en komponent som bruker hooken:

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>
  );
}

Relaterte artikler