How to Create a 2D draggable grid with react-spring

May 29, 20214 mins read

In this article we will create a 2D grid were each item can be dragged and moved to a different place, quick demo.

We will be writing most of the things from scratch to see how things work under the hood but will be using react-spring to animate because it takes the animation out of React for performance reasons! don't worry logic will be still ours, though you can surely remove react-spring out of the picture and use something else or just React ;) , we will see it at the end.

What we will cover, this will be a 3 parts series

  1. Creating a single draggable block
  2. Creating 2D blocks layout with custom hook useDraggable
  3. Rearranging blocks using react-spring [2nd week of June]

Creating a single draggable block

What is a draggable block? block which moves with the mouse pointer, when the mouse key is pressed until the pressure from the key is released.

There are 3 events involved here

  1. Mouse key/track-pad is pressed i.e. mouseDown
  2. Mouse is moved hence the pointer moves i.e. mouseMove
  3. The pressure is released i.e. mouseUp

mouseDown will give us the initial coordinates, on each mouseMove this will be fired whenever there is a movement even for 1px will give us the accurate path and mouseUp will give us the ending coordinates. Our block (it can be anything, div, image etc.) have to move with the mouse, so we will bind appropriate methods with the mouse events.

Let's create a block.

import * as React from "react";
// For CSS in JS
import styled from "styled-components";

const BlockWrapper = styled("div")`
  position: relative;
  border-radius: 4px;
  margin-right: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 120px;
  width: 120px;
  background: red;
`;

const StyledText = styled("p")`
  color: white;
  font-weight: 600;
  font-size: 24px;
`;

const Block = () => {
  return (
    <BlockWrapper>
      <StyledText>1</StyledText>
    </BlockWrapper>
  );
};

export default Block;

Great we have a static block now let move it. Let's apply mouseDown to our block. Before jumping to actual code, let's try to derive the calculation needed.

Block Initial Coordinates: [0,0]

Pointer Initial Coordinates: [10,2] (when mouseDown was fired)

Pointer Final Coordinates: [110,102] (when mouseUp was fired)

Pointer Movement= Final-Initial: [100, 100]

Block Coordinates = [Block Initial Coordinates + Pointer Movement]

Now block may have some initial coordinates, but that will be covered as we are adding the difference to it.

const Block = () => {
  const [coordinate, setCoordinate] = React.useState({
    block: {
      x: 0,
      y: 0,
    },
    pointer: { x: 0, y: 0 },
    dragging: false,
  });

  const handleMouseMove = React.useCallback(
    (event) => {
      if (!coordinate.dragging) {
        return;
      }
      const coordinates = { x: event.clientX, y: event.clientY };

      setCoordinate((prev) => {
        const diff = {
          x: coordinates.x - prev.pointer.x,
          y: coordinates.y - prev.pointer.y,
        };
        return {
          dragging: true,
          pointer: coordinates,
          block: { x: prev.block.x + diff.x, y: prev.block.y + diff.y },
        };
      });
    },
    [coordinate.dragging]
  );

  const handleMouseUp = React.useCallback(() => {
    setCoordinate((prev) => ({
      ...prev,
      dragging: false,
    }));
  }, []);

  const handleMouseDown = React.useCallback((event) => {
    const startingCoordinates = { x: event.clientX, y: event.clientY };
    setCoordinate((prev) => ({
      ...prev,
      pointer: startingCoordinates,
      dragging: true,
    }));
    event.stopPropagation();
  }, []);

  return (
    <BlockWrapper
      style={{ top: coordinate.block.y, left: coordinate.block.x }}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    >
      <StyledText>1</StyledText>
    </BlockWrapper>
  );
};

Try it here.

It's buggy

If the pointer is moved fast enough the block will be lost in the way as now the pointer has crossed the block, onMouseMove doesn't triggers anymore, hence no more dragging, a simple way to fix it is add mousemove and mouseup to document or the parent div.

We cannot add handler directly on document, we have to use addEventListener and with parent we can move our state upward and pass handleMouseUp and handleMouseMove to parent div. Something like this

<div
      style={{ border: "1px solid", height: "100%", width: "100%" }}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}>
      <Block
        style={{ top: coordinate.block.y, left: coordinate.block.x }}
        onMouseDown={handleMouseDown}
      />
</div>

Handlers on Parent

Events on Document

So which one? The parent one, there is two reason behind it:

  1. Not all area of app is going to be draggable, probably one section of it so if the mouse moves out of the parent div, our block will stay inside, but in case of events on document, we have check that on every mouseMove event.
  2. Other reason is handlers are more "React Way" of doing things, also you don't have to remove them ;) on unmount.

That's all for today! Next up we will move our code responsible for dragging into a hook and will create a 2D layout.

It should be noted there are many libraries which provides hook out of the box for dragging, one is use-gesture which works seamlessly with react-spring and also takes dragging out of React, giving a little performance boast. Though we will not be covering it here as our target is to learn the basics.

Sourced from dev.to 🧑‍💻 👩‍💻