Create draggable components with React-Draggable

7 min read 2225

Draggable Components React Draggable

Simple and straightforward to use, the React-Draggable library applies CSS transformations to React components, allowing you to drag components across a UI. With different props that let you change components’ behavior, React-Draggable is a great choice for creating intuitive, user-friendly interfaces.

In this tutorial, we’ll explore React-Draggable in depth by creating a task list with draggable components. Let’s get started!

Installing React-Draggable

Before installing React-Draggable, make sure you have a React project set up on your local machine. Navigate to the project and run the following command in the terminal:

npm install react-draggable

Now, we’ll import the <Draggable/> component from the library. To add movement to an element or a component, we can simply wrap the <Draggable/> component around it.

Add the following code to App.js:

import Draggable from "react-draggable";

function App() {
  return (
 <Draggable>
      <div>I can now be moved around!</div>
    </Draggable>
  );
}

export default App;

The code above will render the following output in the browser:

Draggable Components React Draggable

Now that you know how the React-Draggable library works, let’s get started on our task list.

Setting up a React app

With the React-Draggable library installed, we can set up the required file structure for our project and review the components that we’ll need. To assign unique keys to key components, we’ll need to install a UUID() as follows:

npm i uuid

Our folder structure will look like the image below:

React Draggable Project Folder Structure

Creating the task list

We’ve broken down the logic for our project into three different components. Our task list will use three columns to render their respective tasks.

We’ll add a form element to take input from the user and add the task in the respective component. Our App.js file handles the main task list in its state:

import "./App.css";
import { useState } from "react";

function App() {
const [tasks, setTasks] = useState([]);
  const [addItem, setAddItem] = useState(false);
  const handleSubmit = () => {
    setAddItem(!addItem);
  };
  const addTask = (task) => {
    setTasks(task);
  };
  return (
  <div>
 </div>

  );
}


export default App;

In the code above, we’ve added the following functions:

  • tasks: holds the array of tasks that will be rendered on the screen
  • addItem: the state for showing and hiding the AddForm element, which adds a task

Now that we have the states ready, let’s create the other components.

Header component

Add the following code to Header.js:

import React from "react";

export default function Header({ handleSubmit }) {
  return (
    <div className='header'>
      <button onClick={handleSubmit} className='addButton'>
        Add Task
      </button>
      <p>Planning</p>
      <p>In Progress</p>
      <p>Done</p>
    </div>
  );
}

The </button> gets the handleSubmit function as a prop, which will show the AddForm element. With our header set up, let’s create the AddForm element:

AddForm element

Add the following code to AddTaskForm.js:

import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";

export default function AddTaskForm({ setAddItem, addItem, tasks, setTasks }) {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [option, setOption] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    let newTask = {
      id: uuidv4(),
      name: name,
      description: description,
      timeline: option,
    };
    setTasks([...tasks, newTask]);
    setAddItem(!addItem);
  };
  return (
    <div className='addForm'>
      <form onSubmit={(e) => handleSubmit(e)}>
        <input
          type='text'
          placeholder='Name'
          onChange={(e) => setName(e.target.value)}
        />
        <input
          type='text'
          placeholder='Description'
          onChange={(e) => setDescription(e.target.value)}
        />
        <select
          name='timeline'
          id='timeline'
          onChange={(e) => {
            setOption(e.target.value);
          }}
        >
          <option value=''></option>
          <option value='planning'>Planning</option>
          <option value='inprogress'>In-Progress</option>
          <option value='done'>Done</option>
        </select>
        <button type='submit'>Add</button>
      </form>
    </div>
  );
}

In the code block above, we have input fields that will take relevant data provided by users. Each task has an id, name, date, and timeline , which are taken by the select element.

When the form is submitted, each task will be added to the task list that we created in App.js. When the form is submitted successfully, it will be hidden.

Creating the columns

In our task list, we’ll have three columns titled Planning, In Progress, and Done. Let’s create these components and add their respective code:

Planning.js

import React from "react";
import Card from "./Card";

export default function Planning({ tasks, addTask }) {
  return (
    <>
      {tasks
        .filter((item) => item.timeline === "planning")
        .map((e) => (
          <Card currentTask={e} tasks={tasks} addTask={addTask} />
        ))}
    </>
  );
}

InProgress.js

import React from "react";
import Card from "./Card";

export default function InProgress({ tasks, addTask }) {
  return (
    <>
      {tasks
        .filter((item) => item.timeline === "inprogress")
        .map((e) => (
          <Card currentTask={e} tasks={tasks} addTask={addTask} />
        ))}
    </>
  );
}

Done.js

import React from "react";
import Card from "./Card";

export default function Done({ tasks, addTask }) {
  return (
    <>
      {tasks
        .filter((item) => item.timeline === "done")
        .map((e) => (
          <Card currentTask={e} tasks={tasks} addTask={addTask} />
        ))}
    </>
  );
}

You may have noticed that all of these components hold a similar logic. We are getting the tasks and filtering them based on their timeline using tasks.filter(), which removes the task objects that do not belong in the column.

For example, in the planning component, filter() will remove every object that does not have the planning string in its timeline attribute.

The array returns a result and renders a <Card/> component. Let’s create Card.js in the same directory and add the code snippet below:

import React from "react";
import Draggable from "react-draggable";

export default function Card({ currentTask, tasks, addTask }) {
  return (
    <Draggable grid={[10, 10]} axis='y' bounds='parent'>
      <div className='card' key={currentTask.id}>
        <div className='heading'>
          <h3>{currentTask.name && currentTask.name}</h3>
          <img
            onClick={() => {
              const newTaskList = tasks.filter((item) => {
                if (item.id != currentTask.id) {
                  return item;
                }
              });
              addTask(newTaskList);
            }}
            src='https://toppng.com/uploads/preview/recycling-bin-vector-delete-icon-png-black-11563002079w1isxqyyiv.png'
            style={{ height: "20px", width: "20px" }}
          />
        </div>
        <p>{currentTask.description}</p>
      </div>
    </Draggable>
  );
}

The <Card/> component gets three props, which are required for rendering and deleting logic.



The parent div <Draggable> is imported from our library, making the whole card moveable. By default, you can move the card anywhere on the screen. However, to prevent it from going outside of its parent element, we’ll provide a value to a bounds prop.

Deleting the task

For removing a task, add the code below:

  const newTaskList = tasks.filter((item) => {
    if (item.id != currentTask.id) {
      return item;
    }
  });

  addTask(newTaskList);

The code block above gets the entire list of tasks from App.js, then creates a new list that excludes the current task. Next, let’s import each component in App.js:

import "./App.css";
import { useState } from "react";
import Header from "./Header";
import Planning from "./Planning";
import InProgress from "./InProgress";
import Done from "./Done";
import AddTaskForm from "./AddTaskForm";
function App() {
  const [tasks, setTasks] = useState([]);
  const [addItem, setAddItem] = useState(false);
  const handleSubmit = () => {
    setAddItem(!addItem);
  };
  const addTask = (task) => {
    setTasks(task);
  };
  return (
    <div>
      <Header handleSubmit={handleSubmit} />
      <div className='mainGrid'>
        <div className='column'>
          <Planning tasks={tasks} addTask={addTask} />
        </div>
        <div className='column'>
          <InProgress tasks={tasks} addTask={addTask} />
        </div>
        <div className='column'>
          <Done tasks={tasks} addTask={addTask} />
        </div>
        {addItem && (
          <AddTaskForm
            addItem={addItem}
            setAddItem={setAddItem}
            tasks={tasks}
            setTasks={addTask}
          />
        )}
      </div>
    </div>
  );
}

export default App;

Finally, we’ll add styles to our task list. Add the following code in App.css:

.mainGrid{
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  flex-wrap: wrap;
  height: 100vh;
}
.header{
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  flex-wrap: wrap;
  background-color: rgb(205, 240, 240)
}
.column{
  width: 32vw;
  display: flex;
  flex-direction: column;
  border: 5px solid grey;
  background-color: azure;
  overflow: hidden;
}
.card{
  border: 1px solid black;
  background-color: white;
}
.card p{
  font-size: 10px;
  width: 30vw;
}
.heading{
  display: flex;
  flex-direction:row;
  align-items: center;
  justify-content: space-between;

}
.addButton{
  position:absolute;
  top: 2.5%;
  left: 0%;
}
.addForm{
  height: 50vh;
  width: 50vw;
  background-color: lightblue;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  position: absolute;
  top: 25%;
  left: 25%;
}
.addForm form{
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column; 
}

When we run the project and add an item to our task list, the output will look like the image below:

Final Task List Three Columns-react-draggable

These tasks are moveable within their respective columns:

React Draggable Components Movement

To delete a task, simply click on the delete icon.

Draggable event listeners

The react-draggable API supports a list of callback methods to track the movement of the draggable element:

  • onDrag() — This method listens to the drag event and gets triggered when drag is in process
  • onStart() — This method listens to the drag event and gets triggered when dragging starts
  • onStop() — This method listens to the drag event and gets triggered when dragging stops
  • onMouseDown() — This method gets triggered when the mouse is pressed on a draggable element
  • onMouseUp() — This method gets triggered when the mouse is released on a draggable element
  • onTouchStart() — This method gets triggered in the touch environment before drag start
  • onTouchEnd() — This method gets triggered in the touch environment before drag stops

Event listener usage on draggable components

import Draggable, {DraggableCore} from 'react-draggable';
import React from 'react-dom'
function MyDraggableComponent() {
  const eventHandler = (e, data) => {
    console.log('Event Type', e.type);
    console.log({e, data});
  }

  return (
    <Draggable
        defaultPosition={{x: 0, y: 0}}
        onMouseDown={eventHandler}
        onStart={eventHandler}
        onDrag={eventHandler}
        onStop={eventHandler}>
        <div style={{border: "2px solid red", padding: "1rem", width: "30%"}}>
          <div className="handle">Drag from here</div>
          <div>This readme is really dragging on...</div>
        </div>
      </Draggable>
  )
}

export default MyDraggableComponent;

The handler function receives the event and data objects as parameters.

Additionally, the data object contains information about the current position of the draggable component.

Draggable component props

axis

The axis prop determines on which axis the draggable can move. It receives the following string values:

  • both allows movement horizontally and vertically. This is the default value for the axis
  • x limits movement to the horizontal axis
  • y limits movement to the vertical axis
  • none stops all movement
<Draggable axis="x">
  <div style={{border: "2px solid red", padding: "1rem", width: "30%"}}>
    <div className="handle">Drag from here</div>
    <div>This readme is really dragging on...</div>
  </div>
</Draggable>

handle

The handle prop defines a selector that will be used as the drag handle.

Without the handle prop, the draggable component will be used as the drag handle:

<Draggable handle=".handle">
  <div style={{border: "2px solid red", padding: "1rem", width: "30%"}}>
    <div style={{backgroundColor: "green", width: "30%"}} className="handle">
        Drag from here
    </div>
    <div>This readme is really dragging on...</div>
  </div>
</Draggable>

With the above snippet, we can move the draggable component by clicking on the green background color element.

defaultPosition

The defaultPosition prop specifies the x and y coordinate that the dragged item should start:

<Draggable defaultPosition={{x: 0, y: 0}}>
  <div style={{border: "2px solid red", padding: "1rem", width: "30%"}}>
    <div className="handle">Drag from here</div>
    <div>This readme is really dragging on...</div>
  </div>
</Draggable>

disabled

The disabled prop receives a Boolean value. If true, it will not call any drag handlers, making the draggable component operate like a completely static component.


More great articles from LogRocket:


bounds

The bounds prop specifies the movement boundaries. It receives the following string values:

  • parent — The parent value restricts movement within the node’s offsetParent (the nearest node with position relative or absolute)
  • An object with left, top, right, and bottom properties, which indicate how far in each direction the draggable can be moved
<Draggable bounds={{left: 0, top: 0, right: 100, bottom: 200}}>
  <div style={{border: "2px solid red", padding: "1rem", width: "30%"}}>
    <div className="handle">Drag from here</div>
    <div>This readme is really dragging on...</div>
  </div>
</Draggable>

With the above snippet, the movement of our draggable component will be restricted to the specified values.

For more information on draggable props, check the documentation.

Handling the findDOMNode deprecation error

If the draggable component is running in React Strict mode, the warning ReactDOM.findDOMNode() is deprecated will be logged on the browser’s console.

The fix to avoid this warning is to pass a nodeRef as in this example:

function MyDraggableComponent() {
  const nodeRef = React.useRef(null);
  return (
    <Draggable nodeRef={nodeRef}>
      <div ref={nodeRef}>Example Target</div>
    </Draggable>
  );
}

export default MyDraggableComponent;

Conclusion

Adding movement to components in your application has benefits for your UI like improved visual interest and better organization. The React-Draggable library is a great choice for its simplicity and functionality.

In this tutorial, we used React-Draggable to create a task list. To learn more about React-Draggable, be sure to visit the GitHub page.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now

Leave a Reply