Excerpt

React Dnd is hightly configurable and good library for drag and drop in react. But the implementation is tough to understand from the docs. So we will create a simple drag and drop project in react, and go through the basic concepts like DropTarget, DragSource, DragDropContext and more.

React DnD is a popular library to implement drag and drop in react. It is a low level library, lower than jquery.

Lets start using react dnd

Create a new react app

Use the command to create a new react app with the folder name react-dnd-example-app

npx create-react-app react-dnd-example-app

Then go inside the folder react-dnd-example-app and start the react app

cd <path to folder>/react-dnd-example-app
npm start

So your react app will be started at port 3000. You can open http://localhost:3000 and see the server running.

Install the dependencies

We are going to use react-dnd and react-dnd-html5-backend package. So run the command

npm install --save react-dnd
npm install --save react-dnd-html5-backend

Understanding the project

drag and drop
drag and drop gif

We are going to create a simple project, with multiple draggable sources, and single target. So there are going to be three components

  • Source component - Source component will be draggable component (Block A and Block B)
  • Target component - Source components will be dragged to the target component (the workspace area where we drop block)
  • Container component - Parent component which will manage source and target component

Concepts

React DnD has many configuration and options. Explaining each of them will be overkill for this tutorial. We will only go through the required config to implement out drag and drop feature.

  • DragSource: DragSource is a higher order component which wraps around our source component.
  • DragTarget: DragTarget is the higher order component which wraps around our target component.
  • DragDropContext - DragDropContext wraps around our container component.

Source component

The source container is simple block element as shown in gif.

source.jsx

import React, { Component } from 'react';
import './source.css';
class Source extends Component{
  render(){
    const { name, connectDragSource } = this.props;
    return connectDragSource(
      <div className="square">
        {name}
      </div>
    )
  }
}

source.css

.square{
    width: 100px;
    height: 100px;
    background-color: #eee;
    color: #111;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid #111;
    margin: 16px;
}

We need to wrap this component with DragSource. So instead of export Source, we will export DragSource wrapped Source

export default DragSource("SOURCE", cardSource, collect)(Source);

Here "SOURCE" is the unique id for source component cardSource is the spec file - contract file. It implements the necessary callback events like "beginDrag". collect is the function that gives the drag events access in render function. So we can use isDragging or connectDragSource inside render function.

const cardSource = {
    beginDrag(props, monitor, component) {
        const item = { id: props.id };
        return item;
    }
};
function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource()
  };
}

Complete source component - Source.jsx

import React, { Component } from 'react';
import { DragSource } from 'react-dnd';
import './source.css';

class Source extends Component{
  render(){
    const { name, connectDragSource } = this.props;
    return connectDragSource(
      <div className="square">
        {name}
      </div>
    )
  }
}

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource()
  };
}

const cardSource = {
    beginDrag(props, monitor, component) {
        const item = { id: props.id };
        return item;
    }
};

export default DragSource("SOURCE", cardSource, collect)(Source);

Target component

Target component is a simple compoent which accepts props to render inside component.

class Target extends Component{
    render(){
        const { isOver, canDrop, connectDropTarget, droppedItem } = this.props;
        let className = "";
        if(isOver && canDrop){
            className = 'green';
        }else if(!isOver && canDrop){
            className = 'yellow';
        }else if(isOver && !canDrop){
            className = 'red'
        }
        console.log(droppedItem)
        return connectDropTarget(
            <div className={`target ${className}`}>
                { droppedItem && droppedItem.id }
            </div>
        )
    }
}

We export the Target wrapped with DropTarget.

export default DropTarget("SOURCE", spec, collect)(Target);

Here "SOURCE" is the unique keyword. The target component will only accept dragsource with keyword "SOURCE".

spec function

const spec = {
    drop(props, monitor, component){
        const item = monitor.getItem()
        props.onDrop(item)
    }
}

drop is called when the source is dropped inside the target component. We get the item which is dropped using monitor.getItem(), and call the callback function onDrop of parent component.

collect function

function collect(connect, monitor) {
  return {
      connectDropTarget: connect.dropTarget(),
      isOver: monitor.isOver(),
      isOverCurrent: monitor.isOver({ shallow: true }),
      canDrop: monitor.canDrop(),
      itemType: monitor.getItemType(),
      dropResult: monitor.getDropResult()
  };
}

connectDropTarget is required to handle drop events. The other events like isOver, isOverCurrent are used sinde render function to add or remove classes.

Complete Target component

import React, { Component } from 'react';
import { DropTarget } from 'react-dnd';
import './target.css';

class Target extends Component{
    render(){
        const { isOver, canDrop, connectDropTarget, droppedItem } = this.props;
        let className = "";
        if(isOver && canDrop){
            className = 'green';
        }else if(!isOver && canDrop){
            className = 'yellow';
        }else if(isOver && !canDrop){
            className = 'red'
        }
        console.log(droppedItem)
        return connectDropTarget(
            <div className={`target ${className}`}>
                { droppedItem && droppedItem.id }
            </div>
        )
    }
}

const spec = {
    drop(props, monitor, component){
        const item = monitor.getItem()
        props.onDrop(item)
    }
}
function collect(connect, monitor) {
  return {
      connectDropTarget: connect.dropTarget(),
      isOver: monitor.isOver(),
      isOverCurrent: monitor.isOver({ shallow: true }),
      canDrop: monitor.canDrop()
  };
}

export default DropTarget("SOURCE", spec, collect)(Target);

Container component

Code for container component. It will host the source and target component

class Container extends Component {
  ...
  render() {
    return (
          ...
            <Source name="Block A" id="a"/>
          ...
            <Target 
              droppedItem={this.state.droppedItem} 
              onDrop={this.onDrop}/>
          ...
    );
  }
}

Complete container component

import React, { Component } from 'react';
import Source from './Source.jsx';
import Target from './Target';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import './App.css';

class Container extends Component {
  constructor(){
    super();
    this.state = {
      droppedItem: {}
    }
    this.onDrop = this.onDrop.bind(this);
  }

  onDrop(item){
    this.setState({
      droppedItem: item
    })
  }

  render() {
    return (
      <div className="App">
        <div className="source">
          <Source name="Block A" id="a"/>
          <Source name="Block B" id="b"/>
        </div>
        <div className="destination">
          <Target 
            droppedItem={this.state.droppedItem} 
            onDrop={this.onDrop}/>
        </div>
      </div>
    );
  }
}

export default DragDropContext(HTML5Backend)(Container);

Here we get the dropped item in the onDrop function. We set the state to dropped item, and pass it on to Target component. So the target component is able to display the dropped component.

The code is avaiable in codesandbox