Drag and Drop with React

Are you curious how to use drag and drop with React? If so, this article is exactly for you! Have a good read.

Modern web applications have multiple forms of interaction. Among those, drag and drop is, certainly, one of the most appealing to the user. Apps such as Trello, Google Drive, Office 365 and Jira make heavy use of DnD and users simply love it.

Our example component

To illustrate this article, we’ll implement a jigsaw puzzle in React. The puzzle consists of two boards: the first one with the pieces shuffled, the second one with no pieces and the original image in the background.

The pieces

This is the original image of our puzzle:

We need to split it in multiple pieces. I think 40 is an optimal number, given the proportion of this image:

Then, we save all assets in the images folder:

Now we need to save the original image (non-splitted) in this same folder:

Tip: You don’t need to split the sample image by yourself. Just take a look at this sandbox and you’ll find all needed images right there.

The component’s state

After that, let’s create our component basic structure:

import React, { Component } from 'react';
import originalImage from './images/ny_original.jpg';
import './App.css';

class Jigsaw extends Component {
  state = {
    pieces: [],
    shuffled: [],
    solved: []
  };

  // ...
}

In the above code, we’ve defined the component’s state. It has three arrays:

  1. The pieces array will store objects containing data for each piece of our puzzle.
  2. The shuffled array represents the board where every piece will start at, all shuffled.
  3. The solved array represents the board where all pieces will be dragged to, in the correct order.

Once we’ve defined the structure of our component’s state, we need to set the initial values of each array. We can do this in the componentDidMount lifecycle method:

componentDidMount() {
  const pieces = [...Array(40)]
    .map((_, i) => (
      {
        img: `ny_${('0' + (i + 1)).substr(-2)}.jpg`,
        order: i,
        board: 'shuffled'
      }
    ));

  this.setState({
    pieces,
    shuffled: this.shufflePieces(pieces),
    solved: [...Array(40)]
  });
}

In the above snippet, we’ve initialized the three arrays of our component’s state. Did you notice the […Array(40)] part? We use it twice in this method. It’s leveraging the spread operator to create an iterable array with 40 items.

Each item of the pieces array is an object containing three properties:

  1. The pieces array will store objects containing data for each piece of our puzzle.
  2. The shuffled array represents the board where every piece will start at, all shuffled.
  3. The solved array represents the board where all pieces will be dragged to, in the correct order.

To initialize the shuffled array, we’ve created a shufflePieces method. It randomizes the items of a given array by using the Durstenfeld shuffle algorithm:

shufflePieces(pieces) {
  const shuffled = [...pieces];

  for (let i = shuffled.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }

  return shuffled;
}

Note: You don’t really need to understand this algorithm for the purpose of this article, but if you’re curious to know how it works, here’s a detailed explanation.

The render method

Now let’s see how the render method for this component looks like:

render() {
  return (
    <div className="jigsaw">
      <ul className="jigsaw__shuffled-board">
        {
          this.state.shuffled.map((piece, i) =>
          this.renderPieceContainer(piece, i, 'shuffled'))
        }
      </ul>
      <ol className="jigsaw__solved-board" style={{ backgroundImage: `url(${originalImage})` }}>
        {
          this.state.solved.map((piece, i) =>
          this.renderPieceContainer(piece, i, 'solved'))
        }
      </ol>
    </div>
  );
}

As you can see, we create two lists, one for each board. The piece’s rendering logic was extracted to a separate method. Here it is:

renderPieceContainer(piece, index, boardName) {
  return (
    <li key={index}>
      {piece && <img src={require(`./images/${piece.img}`)}/>}
    </li>
  );
}

This method uses short circuit evaluation to render the piece image conditionally.

I’ve added some styling to make it look nice. In this article, I won’t cover how it was made, but you can find the complete CSS file in this sandbox.

And here’s how our component looks:

It looks good. Doesn’t it? However, it does nothing for now. The next step is to implement the drag and drop logic itself.

Drag and drop: first things first

To make it possible to drag a piece, we need to mark it with a special attribute called draggable:

renderPieceContainer(piece, index, boardName) {
  return (
    <li key={index}>
      {
        piece && <img
          draggable
          src={require(`./images/${piece.img}`)}/>
      }
    </li>
  );
}

After that, we need to create a handler for the onDragStart event of our pieces:

piece && <img
  draggable
  onDragStart={(e) => this.handleDragStart(e, piece.order)}
  src={require(`./images/${piece.img}`)}/>


And here’s the handler itself:

handleDragStart(e, order) {
  e.dataTransfer.setData('text/plain', order);
}

This is what the above snippets do:

  1. The pieces array will store objects containing data for each piece of our puzzle.
  2. The shuffled array represents the board where every piece will start at, all shuffled.
  3. The solved array represents the board where all pieces will be dragged to, in the correct order.

The drag part is done. But dragging a piece is useless if you have no place to drop it. The first thing to do in order to make pieces droppable is to define a handler for the onDragOver event of our piece containers (the li tags of both lists). This handler will be very simple. So simple that, in fact, we can define it inline:

renderPieceContainer(piece, index, boardName) {
  return (
    <li key={index}
      onDragOver={(e) => e.preventDefault()}>

  // ...
}

By default, most zones of a page won’t be a valid dropping place. This is the reason why the normal behavior of the onDragOver event is to disallow dropping. To overcome this, we’re calling the event.preventDefault() method.

Drag and drop: let’s finally drop it!

Finally, let’s implement the dropping logic. To do this, we must define a handler for the onDrop event of our piece containers:

renderPieceContainer(piece, index, boardName) {
  return (
    <li
      key={index}
      onDragOver={(e) => e.preventDefault()}
      onDrop={(e) => this.handleDrop(e, index, boardName)}>

    // ...
}

Now, the handler code:

handleDrop(e, index, targetName) {
  let target = this.state[targetName];
  if (target[index]) return;

  const pieceOrder = e.dataTransfer.getData('text');
  const pieceData = this.state.pieces.find(p => p.order === +pieceOrder);
  const origin = this.state[pieceData.board];

  if (targetName === pieceData.board) target = origin;
  origin[origin.indexOf(pieceData)] = undefined;
  target[index] = pieceData;
  pieceData.board = targetName;
  this.setState({ [pieceData.board]: origin, [targetName]: target })
}

Let’s understand what’s happening here:

  1. The pieces array will store objects containing data for each piece of our puzzle.
  2. The shuffled array represents the board where every piece will start at, all shuffled.
  3. The solved array represents the board where all pieces will be dragged to, in the correct order.

And here’s the puzzle working:

Take a look at the final sandbox.

Conclusion

  • To make an element draggable, mark it with the draggable attribute.
  • Use the onDragStart event to store data for uniquely identifying the item being dragged.
  • By default, most areas of a page won’t allow dropping. To fix this, define a handler for the onDragOver event and call the event.preventDefault() method inside it.
  • Finally, to finish the dropping process, create a handler for the onDrop event. In this method, update your component’s state to reflect the desired dropping behavior.

#reactjs

Drag and Drop with React
92.75 GEEK