Compartir en Twitter
Go to Homepage

IMPLEMENTAR DRAG AND DROP EN REACT SIN LIBRERÍAS

November 30, 2025

Introducción a Drag and Drop en React

La funcionalidad de arrastrar y soltar (drag and drop) es una característica esencial en aplicaciones web modernas, ya que mejora la interactividad y la experiencia del usuario. En el contexto de React, implementar esta funcionalidad sin depender de librerías externas permite un mayor control sobre el código y reduce las dependencias del proyecto. Este tutorial detalla cómo lograrlo utilizando las APIs nativas de HTML5, específicamente los eventos de arrastre, para construir una aplicación que permita mover tareas entre diferentes categorías, como “en progreso” y “completado”. A través de ejemplos prácticos, se explorarán los fundamentos de los eventos de arrastre, la gestión de estado en React y la creación de componentes reutilizables.

El enfoque presentado aprovecha las capacidades de React para gestionar el estado y los eventos, combinándolas con la API de Drag and Drop de HTML5. Esto garantiza una solución ligera y adaptable, adecuada para aplicaciones donde la personalización es prioritaria. Este tutorial está diseñado para desarrolladores con conocimientos básicos de React y JavaScript, y se enfoca en un caso práctico: una aplicación de tareas que permite reorganizar elementos mediante arrastre.

Configuración del Proyecto

Para comenzar, es necesario configurar un proyecto de React. Utilizaremos Create React App para generar la estructura inicial, ya que es una herramienta ampliamente adoptada que simplifica el proceso de configuración. A continuación, se detallan los pasos para crear el proyecto:

npx create-react-app drag-and-drop-demo
cd drag-and-drop-demo
npm start

Este comando genera un proyecto con una estructura básica y lanza el servidor de desarrollo. La aplicación estará disponible en http://localhost:3000. La estructura inicial incluye un componente principal App.js, que servirá como punto de partida para implementar la funcionalidad de arrastre.

Creación del Componente Principal

El componente principal, AppDragDropDemo, será el encargado de gestionar la lógica de arrastre y la interfaz de usuario. Este componente se construirá como una clase, ya que permite manejar el estado y los métodos de ciclo de vida de manera clara. A continuación, se muestra el código inicial del componente:

import React, { Component } from "react";
export default class AppDragDropDemo extends Component {
    render() {
        return <div className="container-drag">DEMO DE ARRASTRAR Y SOLTAR</div>;
    }
}

Este código renderiza un contenedor simple con un mensaje. Para integrarlo en la aplicación, actualizamos el archivo index.js:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import AppDragDropDemo from "./AppDragDropDemo";
ReactDOM.render(<AppDragDropDemo />, document.getElementById("root"));

Al ejecutar la aplicación, se mostrará un contenedor con el texto “DEMO DE ARRASTRAR Y SOLTAR”. Este es el punto de partida para agregar la funcionalidad de arrastre.

Definición de Tareas y Estado Inicial

Para simular una aplicación de gestión de tareas, definimos un conjunto de tareas que se podrán arrastrar entre categorías como “en progreso” (wip) y “completado” (complete). El estado del componente almacenará estas tareas como un arreglo de objetos, cada uno con un nombre y una categoría. A continuación, se muestra cómo inicializar el estado en el constructor del componente:

constructor(props) {
  super(props);
  this.state = {
    tasks: [
      { name: "Aprender JavaScript", category: "wip" },
      { name: "Dominar React", category: "wip" },
      { name: "Construir API", category: "complete" },
      { name: "Testear aplicación", category: "complete" }
    ]
  };
}

El estado tasks contiene cuatro tareas, dos en la categoría “wip” y dos en “complete”. Este arreglo será la base para renderizar los elementos arrastrables y las zonas donde se pueden soltar.

Renderizado de Tareas

El siguiente paso es renderizar las tareas en el componente, organizándolas en contenedores según su categoría. Cada tarea será un elemento <div> con el atributo draggable para habilitar el arrastre. Además, cada contenedor será una zona donde se puedan soltar las tareas. A continuación, se muestra el método render actualizado:

render() {
  var tasks = {
    wip: [],
    complete: []
  };

  this.state.tasks.forEach((t) => {
    tasks[t.category].push(
      <div
        key={t.name}
        className="draggable"
        draggable
      >
        {t.name}
      </div>
    );
  });

  return (
    <div className="container-drag">
      <h2 className="header">DEMO DE ARRASTRAR Y SOLTAR</h2>
      <div className="wip">
        <span className="task-header">EN PROGRESO</span>
        {tasks.wip}
      </div>
      <div className="droppable complete">
        <span className="task-header">COMPLETADO</span>
        {tasks.complete}
      </div>
    </div>
  );
}

En este código, se crea un objeto tasks que agrupa los elementos según su categoría. Cada tarea se renderiza como un <div> con la clase draggable y el atributo draggable="true". Los contenedores tienen clases como wip y complete para diferenciarlos visualmente.

Estilización de la Interfaz

Para mejorar la experiencia del usuario, aplicamos estilos CSS que definan la apariencia de los contenedores y los elementos arrastrables. Los estilos se agregan en el archivo index.css:

body {
    background: #d3d3d3;
    font-family: Arial, Helvetica, sans-serif;
}

.container-drag {
    text-align: center;
}

.wip,
.complete {
    position: absolute;
    width: 40%;
    height: 70%;
    top: 15%;
    background: white;
    border: 1px solid #d3d3d3;
}

.wip {
    left: 5%;
}

.complete {
    right: 5%;
}

.header {
    position: absolute;
    top: 5%;
    width: 100%;
    text-align: center;
    font-size: 1.5em;
    font-weight: bold;
    color: #444;
}

.task-header {
    display: inline-block;
    background-color: rgb(0, 151, 230);
    width: 100%;
    color: white;
}

.draggable {
    width: 100px;
    height: 100px;
    background-color: yellow;
    margin: 5px auto;
    line-height: 100px;
}

Estos estilos posicionan los contenedores a los lados de la pantalla, con un fondo blanco y un encabezado azul para cada categoría. Los elementos arrastrables son cuadrados amarillos, lo que facilita su identificación.

Implementación de Eventos de Arrastre

La API de Drag and Drop de HTML5 proporciona eventos como dragstart, dragover y drop para gestionar el proceso de arrastre. Comenzamos implementando el evento dragstart para almacenar el identificador de la tarea que se está arrastrando. Agregamos un manejador en el componente:

onDragStart = (ev, id) => {
    ev.dataTransfer.setData("id", id);
};

Este manejador se asocia a cada elemento arrastrable en el método render:

this.state.tasks.forEach((t) => {
    tasks[t.category].push(
        <div
            key={t.name}
            onDragStart={(e) => this.onDragStart(e, t.name)}
            className="draggable"
            draggable
        >
            {t.name}
        </div>
    );
});

El evento dragstart almacena el nombre de la tarea en el objeto dataTransfer, que estará disponible cuando la tarea se suelte. Para verificar que el evento funciona, podemos inspeccionar los registros en la consola del navegador al arrastrar un elemento.

Configuración de Zonas de Soltado

Para que los contenedores acepten elementos arrastrados, implementamos los eventos dragover y drop. El evento dragover debe prevenir el comportamiento predeterminado del navegador para permitir el soltado:

onDragOver = (ev) => {
    ev.preventDefault();
};

El evento drop actualiza el estado de la tarea, cambiando su categoría según el contenedor donde se suelte:

onDrop = (ev, cat) => {
    let id = ev.dataTransfer.getData("id");
    let tasks = this.state.tasks.filter((task) => {
        if (task.name === id) {
            task.category = cat;
        }
        return task;
    });
    this.setState({
        ...this.state,
        tasks,
    });
};

Estos manejadores se asocian a los contenedores en el método render:

return (
    <div className="container-drag">
        <h2 className="header">DEMO DE ARRASTRAR Y SOLTAR</h2>
        <div
            className="wip"
            onDragOver={(e) => this.onDragOver(e)}
            onDrop={(e) => this.onDrop(e, "wip")}
        >
            <span className="task-header">EN PROGRESO</span>
            {tasks.wip}
        </div>
        <div
            className="droppable complete"
            onDragOver={(e) => this.onDragOver(e)}
            onDrop={(e) => this.onDrop(e, "complete")}
        >
            <span className="task-header">COMPLETADO</span>
            {tasks.complete}
        </div>
    </div>
);

Con estos cambios, los contenedores ahora aceptan tareas arrastradas, y el estado se actualiza para reflejar la nueva categoría de la tarea.

Código Completo del Componente

A continuación, se presenta el código completo del componente AppDragDropDemo, integrando todos los elementos descritos:

import React, { Component } from "react";
export default class AppDragDropDemo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            tasks: [
                { name: "Aprender JavaScript", category: "wip" },
                { name: "Dominar React", category: "wip" },
                { name: "Construir API", category: "complete" },
                { name: "Testear aplicación", category: "complete" },
            ],
        };
    }

    onDragStart = (ev, id) => {
        ev.dataTransfer.setData("id", id);
    };

    onDragOver = (ev) => {
        ev.preventDefault();
    };

    onDrop = (ev, cat) => {
        let id = ev.dataTransfer.getData("id");
        let tasks = this.state.tasks.filter((task) => {
            if (task.name === id) {
                task.category = cat;
            }
            return task;
        });
        this.setState({
            ...this.state,
            tasks,
        });
    };

    render() {
        var tasks = {
            wip: [],
            complete: [],
        };

        this.state.tasks.forEach((t) => {
            tasks[t.category].push(
                <div
                    key={t.name}
                    onDragStart={(e) => this.onDragStart(e, t.name)}
                    className="draggable"
                    draggable
                >
                    {t.name}
                </div>
            );
        });

        return (
            <div className="container-drag">
                <h2 className="header">DEMO DE ARRASTRAR Y SOLTAR</h2>
                <div
                    className="wip"
                    onDragOver={(e) => this.onDragOver(e)}
                    onDrop={(e) => this.onDrop(e, "wip")}
                >
                    <span className="task-header">EN PROGRESO</span>
                    {tasks.wip}
                </div>
                <div
                    className="droppable complete"
                    onDragOver={(e) => this.onDragOver(e)}
                    onDrop={(e) => this.onDrop(e, "complete")}
                >
                    <span className="task-header">COMPLETADO</span>
                    {tasks.complete}
                </div>
            </div>
        );
    }
}

Este código implementa una aplicación completa de arrastre y soltado, con tareas que se pueden mover entre las categorías “en progreso” y “completado”.

Optimización de la Interfaz de Usuario

Para mejorar la experiencia del usuario, podemos agregar retroalimentación visual durante el arrastre. Por ejemplo, podemos cambiar el estilo del contenedor cuando un elemento se arrastra sobre él. Actualizamos el CSS para agregar un borde resaltado:

.wip:hover,
.complete:hover {
    border: 2px dashed #000;
}

Además, podemos modificar el estilo del elemento arrastrable mientras se mueve, reduciendo su opacidad:

.draggable.dragging {
    opacity: 0.5;
}

Para aplicar la clase dragging, agregamos un manejador para el evento drag:

onDrag = (ev) => {
    ev.target.classList.add("dragging");
};

onDragEnd = (ev) => {
    ev.target.classList.remove("dragging");
};

Y actualizamos los elementos arrastrables en el método render:

<div
    key={t.name}
    onDragStart={(e) => this.onDragStart(e, t.name)}
    onDrag={(e) => this.onDrag(e)}
    onDragEnd={(e) => this.onDragEnd(e)}
    className="draggable"
    draggable
>
    {t.name}
</div>

Estas mejoras hacen que la interfaz sea más intuitiva, proporcionando retroalimentación visual clara durante el arrastre.

Manejo de Errores y Casos Límite

Es importante considerar casos límite, como cuando se intenta soltar una tarea en un contenedor no válido o cuando no se encuentra el identificador de la tarea. Podemos agregar una validación en el manejador onDrop:

onDrop = (ev, cat) => {
    let id = ev.dataTransfer.getData("id");
    if (!id) return; // Evitar errores si no hay ID
    let tasks = this.state.tasks.filter((task) => {
        if (task.name === id) {
            task.category = cat;
        }
        return task;
    });
    this.setState({
        ...this.state,
        tasks,
    });
};

Esta validación asegura que el código no falle si el objeto dataTransfer no contiene un identificador válido. Además, podemos prevenir el arrastre de elementos no deseados agregando un tipo de datos específico en dragstart:

onDragStart = (ev, id) => {
    ev.dataTransfer.setData("text/plain", id);
    ev.dataTransfer.effectAllowed = "move";
};

Esto restringe el arrastre a operaciones de movimiento, mejorando la robustez de la aplicación.

Escalabilidad y Reutilización

Para hacer el componente más escalable, podemos refactorizarlo para soportar múltiples categorías dinámicamente. En lugar de tener contenedores fijos para “wip” y “complete”, definimos un arreglo de categorías en el estado:

constructor(props) {
  super(props);
  this.state = {
    categories: ["wip", "complete", "pendiente"],
    tasks: [
      { name: "Aprender JavaScript", category: "wip" },
      { name: "Dominar React", category: "wip" },
      { name: "Construir API", category: "complete" },
      { name: "Testear aplicación", category: "pendiente" }
    ]
  };
}

El método render se adapta para iterar sobre las categorías:

render() {
  var tasks = {};
  this.state.categories.forEach((cat) => {
    tasks[cat] = [];
  });

  this.state.tasks.forEach((t) => {
    tasks[t.category].push(
      <div
        key={t.name}
        onDragStart={(e) => this.onDragStart(e, t.name)}
        onDrag={(e) => this.onDrag(e)}
        onDragEnd={(e) => this.onDragEnd(e)}
        className="draggable"
        draggable
      >
        {t.name}
      </div>
    );
  });

  return (
    <div className="container-drag">
      <h2 className="header">DEMO DE ARRASTRAR Y SOLTAR</h2>
      {this.state.categories.map((cat) => (
        <div
          key={cat}
          className={`droppable ${cat}`}
          onDragOver={(e) => this.onDragOver(e)}
          onDrop={(e) => this.onDrop(e, cat)}
        >
          <span className="task-header">{cat.toUpperCase()}</span>
          {tasks[cat]}
        </div>
      ))}
    </div>
  );
}

Este enfoque permite agregar nuevas categorías sin modificar la lógica principal, haciendo el componente más reutilizable.

Pruebas y Depuración

Para garantizar que la funcionalidad de arrastre funcione correctamente, es recomendable realizar pruebas manuales y automatizadas. Durante el desarrollo, podemos usar la consola del navegador para depurar los eventos:

onDragStart = (ev, id) => {
    console.log("Iniciando arrastre:", id);
    ev.dataTransfer.setData("text/plain", id);
    ev.dataTransfer.effectAllowed = "move";
};

onDrop = (ev, cat) => {
    let id = ev.dataTransfer.getData("text/plain");
    console.log("Soltando en:", cat, "ID:", id);
    if (!id) return;
    let tasks = this.state.tasks.filter((task) => {
        if (task.name === id) {
            task.category = cat;
        }
        return task;
    });
    this.setState({
        ...this.state,
        tasks,
    });
};

Estos registros ayudan a identificar problemas, como eventos que no se disparan o datos incorrectos en dataTransfer. Para pruebas automatizadas, herramientas como React Testing Library pueden simular eventos de arrastre, aunque requieren configuraciones adicionales para la API de HTML5.

Consideraciones de Accesibilidad

La API de Drag and Drop de HTML5 tiene limitaciones en accesibilidad, especialmente para usuarios que dependen de teclados o lectores de pantalla. Para mejorar la accesibilidad, podemos agregar soporte para interacciones mediante teclado. Por ejemplo, permitimos mover tareas con las teclas de flecha:

handleKeyDown = (ev, name) => {
    if (ev.key === "ArrowRight") {
        this.setState({
            tasks: this.state.tasks.map((task) =>
                task.name === name ? { ...task, category: "complete" } : task
            ),
        });
    } else if (ev.key === "ArrowLeft") {
        this.setState({
            tasks: this.state.tasks.map((task) =>
                task.name === name ? { ...task, category: "wip" } : task
            ),
        });
    }
};

Y actualizamos los elementos arrastrables para incluir el evento onKeyDown:

<div
    key={t.name}
    onDragStart={(e) => this.onDragStart(e, t.name)}
    onDrag={(e) => this.onDrag(e)}
    onDragEnd={(e) => this.onDragEnd(e)}
    onKeyDown={(e) => this.handleKeyDown(e, t.name)}
    className="draggable"
    draggable
    tabIndex={0}
>
    {t.name}
</div>

El atributo tabIndex={0} hace que los elementos sean enfocables, permitiendo la navegación por teclado. Esto mejora la accesibilidad, aunque se recomienda complementar con ARIA para entornos más complejos.

Conclusiones

Implementar la funcionalidad de arrastrar y soltar en React sin librerías externas es una tarea alcanzable gracias a la API de Drag and Drop de HTML5 y la gestión de estado de React. Este tutorial ha demostrado cómo construir una aplicación de tareas que permite mover elementos entre categorías, con un enfoque en la simplicidad y la personalización. Al aprovechar eventos como dragstart, dragover y drop, junto con un diseño modular, los desarrolladores pueden crear interfaces interactivas sin aumentar las dependencias del proyecto. Las mejoras adicionales, como retroalimentación visual, manejo de errores y soporte para accesibilidad, aseguran una experiencia robusta y amigable para el usuario. Este enfoque es ideal para proyectos que requieren un alto grado de control y flexibilidad, y puede adaptarse a casos de uso más complejos, como tableros Kanban o editores visuales.