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.
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.
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.
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:
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:
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.
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.
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:
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.
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:
And here’s the puzzle working:
Take a look at the final sandbox.
#reactjs