Sortable Targets with React DnD

March 13, 201610 min read

Last reviewed December 27, 2018

If you ever had to add some drag and drop functionality to your app, chances are you suffered a bit. For those building their apps with React, however, React DnD does not fall in this category. The API is clean, the docs are comprehensive and there are many examples.

This library provides a thin layer to cover your components. First add a drag and drop context, then start connecting your components with the functionality you want. If you haven’t had a chance to test it yet, go there and fiddle around. Read the docs to understand the mindset behind.

The aim of this post is not to explain or advocate for React DnD (I’m positive his author, Dan Abramov, would do a better job). I will also steer clear from Babel and Webpack configuration (let’s reduce the tooling overhead). Instead, I would like to focus on the code and add a new use case.

But if you do want to know the insides, I got you covered: there’s a reproducible repository set on GitHub.

The Problem

Imagine you have many elements and you want to drag and drop them into different containers. You are also asked to sort them. How one would solve this?

The docs provided almost what we want. There is a sortable example and a single target example. Our goal is to combine them and build a sortable multi targets example.

If you still are having problems picturing this – don’t worry! Check the example right below.

Note that every container is a target. When dropping elements into other containers, they are pushed to the end. When inside, they are sorted. Pretty simple.

The Rationale

I’ll not extend myself too much because there isn’t anything really new here. If you have understood the examples I cited before, you will have no trouble in following this. Saying that, my focus will be in how to go from the examples to the app I just presented.

There are 3 components in our app:

  • App, a container component that is connected to the application and plugs the DragDropContext;
  • Container, a component that will hold every card and act like a target for other cards;
  • Card, a component that will be dragged around (either to other Containers or inside the one it is in).

Now should be simple to identify that we have one drag source (the Card) and two drop targets (the Container and also the Card). When dragging Cards outside its Container, we need to remove it to its original Container and push to the new. When dragging inside, sort them.

Let’s start from the beginning.

The App Component

This component is straightforward. Just render the Containers on the screen and give them some props. What props?

Well, definitely the Container should know which cards it is holding. This should be one.

Also, we need a way to distinguish Containers. Remember that we sort Cards being dragged inside its parent Container and push when it’s a new one. So we might pass an id as well.

Our App component should be something like this:

import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';

class App extends Component {

	render() {
		const style = {
			display: "flex",
			justifyContent: "space-around",
			paddingTop: "20px"
		}

		const listOne = [
			{ id: 1, text: "Item 1" },
			{ id: 2, text: "Item 2" },
			{ id: 3, text: "Item 3" }
		];

		const listTwo = [
			{ id: 4, text: "Item 4" },
			{ id: 5, text: "Item 5" },
			{ id: 6, text: "Item 6" }
		];

		const listThree = [
			{ id: 7, text: "Item 7" },
			{ id: 8, text: "Item 8" },
			{ id: 9, text: "Item 9" }
		];

		return (
			<div style={{...style}}>
				<Container id={1} list={listOne} />
				<Container id={2} list={listTwo} />
				<Container id={3} list={listThree} />
			</div>
		);
	}
}

export default DragDropContext(HTML5Backend)(App);

We are defining some style using Flexbox, hardcoding our lists and creating our Containers. Note the last line, when we tell React that App is a DragDropContext with HTML5 Backend. Check the docs if you want a more in-depth explanation.

The Container Component

This is the main component, responsible for managing the state (cards currently inside him) and being a drop target.

Note that we change the Container state in 3 different situations:

  • A Card is pushed, meaning we need to add it to the state;
  • A Card is pushed into another container, meaning we need to remove it from the state.
  • A Card is moved inside the Container, meaning we need to sort it.

This is how I would implement it:

import React, { Component } from 'react';
import update from 'react/lib/update';

class Container extends Component {

	constructor(props) {
		super(props);		
		this.state = { cards: props.list };
	}

	pushCard(card) {
		this.setState(update(this.state, {
			cards: {
				$push: [ card ]
			}
		}));
	}

	removeCard(index) {		
		this.setState(update(this.state, {
			cards: {
				$splice: [
					[index, 1]
				]
			}
		}));
	}

	moveCard(dragIndex, hoverIndex) {
		const { cards } = this.state;		
		const dragCard = cards[dragIndex];

		this.setState(update(this.state, {
			cards: {
				$splice: [
					[dragIndex, 1],
					[hoverIndex, 0, dragCard]
				]
			}
		}));
	}
}

Each method should be straightforward. We are using React’s update helper, a handy Immutability Helper that I strongly recommend you to learn. Some methods might sound cumbersome, but once you get the grasp you follow through pretty easily.

Also note that the moveCard method is identical to the one provided in the sortable example. Talk about reusability!

Now let’s write the render method for our class:

import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';

class Container extends Component {

	...

	render() {
		const { cards } = this.state;
		const { canDrop, isOver, connectDropTarget } = this.props;
		const isActive = canDrop && isOver;
		const style = {
			width: "200px",
			height: "404px",
			border: '1px dashed gray'
		};

		const backgroundColor = isActive ? 'lightgreen' : '#FFF';

		return connectDropTarget(
			<div style={{...style, backgroundColor}}>
				{cards.map((card, i) => {
					return (
						<Card 
							key={card.id}
							index={i}
							listId={this.props.id}
							card={card}														
							removeCard={this.removeCard.bind(this)}
							moveCard={this.moveCard.bind(this)} />
					);
				})}
			</div>
		);
  }
}

There is nothing really new here. Some background styling and an iteration in order to render the Card component and connectDropTarget to tell we might expect some dropping to happen here.

Check that we are passing listId, removeCard and moveCard as props. This is necessary because we have to know in which Container we are, as well wich actions we need to perform. Note that pushCard is an event related to the Component, so we don’t need Card to handle it.

Finally, let’s wrap this class up:

import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';

class Container extends Component {
...
}

const cardTarget = {
	drop(props, monitor, component ) {
		const { id } = props;
		const sourceObj = monitor.getItem();		
		if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
		return {
			listId: id
		};
	}
}

export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
	connectDropTarget: connect.