Compartir en Twitter
Go to Homepage

TUTORIAL DE DRAG AND DROP EN REACTJS SIN LIBRERÍAS

December 6, 2025

Introducción a la funcionalidad de arrastrar y soltar en ReactJS

La funcionalidad de arrastrar y soltar, conocida como drag and drop, es una característica esencial en aplicaciones web modernas que buscan mejorar la experiencia de usuario. Permite a los usuarios mover elementos de un lugar a otro de forma intuitiva, como reorganizar tareas en un tablero Kanban o cargar archivos en una interfaz. En este tutorial, exploraremos cómo implementar esta funcionalidad en ReactJS sin depender de librerías externas, utilizando únicamente la API de HTML5 Drag and Drop y las capacidades nativas de React. Este enfoque no solo reduce las dependencias de tu proyecto, sino que también te otorga un control total sobre el comportamiento y la personalización de la interfaz. A lo largo de este artículo, construiremos una aplicación de demostración que simula un tablero de tareas, con ejemplos de código claros y explicaciones detalladas para cada paso. Este tutorial está diseñado para desarrolladores con conocimientos básicos de React, como el uso de componentes funcionales, estados y eventos.

El objetivo es crear una aplicación que permita a los usuarios arrastrar tareas entre dos categorías: “En progreso” y “Completado”. Usaremos eventos de la API de HTML5 como dragstart, dragover y drop, junto con la gestión de estado en React para actualizar la interfaz dinámicamente. Además, optimizaremos el código para que sea compatible con las versiones más recientes de React en 2025, asegurando un rendimiento eficiente y una experiencia fluida.

Configuración inicial del proyecto

Para comenzar, necesitamos un proyecto de React. Puedes crearlo utilizando herramientas modernas como Vite, que es rápido y ampliamente adoptado en 2025. A continuación, se muestra el comando para inicializar un proyecto con Vite:

npm create vite@latest drag-drop-demo -- --template react
cd drag-drop-demo
npm install
npm run dev

Este comando genera un proyecto básico de React con Vite. Una vez configurado, abre el proyecto en tu editor de código y limpia el contenido predeterminado del archivo src/App.jsx para empezar desde cero. También puedes usar Create React App si prefieres, pero Vite es más ligero y está optimizado para el desarrollo moderno.

El directorio inicial del proyecto tendrá una estructura similar a esta:

drag-drop-demo/
├── public/
├── src/
│   ├── App.jsx
│   ├── main.jsx
│   ├── index.css
│   └── App.css
├── package.json
└── vite.config.js

Asegúrate de que tu entorno de desarrollo esté configurado con Node.js (versión 18 o superior recomendada) y un navegador moderno que soporte la API de HTML5 Drag and Drop, como Chrome, Firefox o Edge.

Creación del componente principal

Comenzaremos creando un componente funcional en src/App.jsx que servirá como el contenedor principal de nuestra aplicación de arrastrar y soltar tareas. Este componente gestionará el estado de las tareas y renderizará las categorías donde se podrán mover los elementos.

Primero, definimos un estado inicial con un arreglo de tareas, cada una con un nombre, una categoría y un color de fondo para distinguirlas visualmente. Usaremos el hook useState para manejar este estado.

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

const AppDragDropDemo = () => {
    const [tasks, setTasks] = useState([
        { name: "Aprender React", category: "wip", bgcolor: "yellow" },
        { name: "Dominar JavaScript", category: "wip", bgcolor: "pink" },
        {
            name: "Explorar TypeScript",
            category: "complete",
            bgcolor: "skyblue",
        },
    ]);

    return (
        <div className="container-drag">
            <h2>DEMO DE ARRASTRAR Y SOLTAR</h2>
        </div>
    );
};

export default AppDragDropDemo;

En este código, inicializamos un arreglo de tareas con tres elementos. Cada tarea tiene un name (nombre de la tarea), una category (que puede ser wip o complete) y un bgcolor para estilizar los elementos. Por ahora, solo renderizamos un contenedor con un título, pero pronto añadiremos las categorías y los elementos arrastrables.

Para conectar este componente a la aplicación, actualiza src/main.jsx para renderizar AppDragDropDemo:

import React from "react";
import ReactDOM from "react-dom/client";
import AppDragDropDemo from "./AppDragDropDemo";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <AppDragDropDemo />
    </React.StrictMode>
);

Estilizando la interfaz

Antes de implementar la lógica de arrastrar y soltar, definamos algunos estilos básicos en src/App.css para que la aplicación sea visualmente clara. Queremos que las categorías “En progreso” y “Completado” se muestren como columnas separadas, con tareas que se puedan distinguir fácilmente.

.container-drag {
    text-align: center;
    padding: 20px;
}

.wip,
.droppable {
    width: 250px;
    min-height: 400px;
    display: inline-block;
    vertical-align: top;
    margin: 10px;
    border: 2px solid #ccc;
    border-radius: 5px;
    background-color: #f9f9f9;
}

.task-header {
    background-color: #333;
    color: white;
    padding: 10px;
    font-weight: bold;
}

.draggable {
    padding: 10px;
    margin: 10px;
    border: 1px solid #aaa;
    border-radius: 5px;
    cursor: move;
}

.draggable.dragging {
    opacity: 0.5;
}

Estos estilos crean un contenedor principal centrado, dos columnas para las categorías y un diseño para los elementos arrastrables. La clase .dragging reduce la opacidad de un elemento mientras se arrastra, mejorando la experiencia visual.

Renderizando las tareas por categoría

Ahora, modifiquemos el componente para renderizar las tareas agrupadas por categoría. Iteraremos sobre el arreglo de tareas y las organizaremos en dos listas: una para wip (en progreso) y otra para complete (completado). Cada tarea será un elemento div con el atributo draggable para habilitar la funcionalidad de arrastrar.

Actualiza el método de renderizado en AppDragDropDemo:

const AppDragDropDemo = () => {
    const [tasks, setTasks] = useState([
        { name: "Aprender React", category: "wip", bgcolor: "yellow" },
        { name: "Dominar JavaScript", category: "wip", bgcolor: "pink" },
        {
            name: "Explorar TypeScript",
            category: "complete",
            bgcolor: "skyblue",
        },
    ]);

    const onDragStart = (e, name) => {
        e.dataTransfer.setData("text/plain", name);
    };

    const onDragOver = (e) => {
        e.preventDefault();
    };

    const onDrop = (e, cat) => {
        const name = e.dataTransfer.getData("text/plain");
        setTasks(
            tasks.map((task) =>
                task.name === name ? { ...task, category: cat } : task
            )
        );
    };

    const tasksByCategory = {
        wip: [],
        complete: [],
    };

    tasks.forEach((t) => {
        tasksByCategory[t.category].push(
            <div
                key={t.name}
                onDragStart={(e) => onDragStart(e, t.name)}
                draggable
                className="draggable"
                style={{ backgroundColor: t.bgcolor }}
            >
                {t.name}
            </div>
        );
    });

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

export default AppDragDropDemo;

Este código introduce varias mejoras. Primero, organizamos las tareas en un objeto tasksByCategory para separarlas por categoría. Luego, añadimos los eventos onDragStart, onDragOver y onDrop para manejar la funcionalidad de arrastrar y soltar elementos. Cada tarea es un div con el atributo draggable y un estilo dinámico basado en su bgcolor. Las áreas de destino (wip y complete) tienen eventos para aceptar elementos soltados.

Implementación de los eventos de arrastrar y soltar

La API de HTML5 Drag and Drop utiliza varios eventos clave para gestionar la interacción. A continuación, explicamos cada uno y su implementación en el código anterior:

  1. dragstart: Se dispara cuando el usuario comienza a arrastrar un elemento. En nuestra aplicación, usamos onDragStart para almacenar el nombre de la tarea en el objeto dataTransfer. Esto permite identificar qué tarea se está moviendo.
const onDragStart = (e, name) => {
    e.dataTransfer.setData("text/plain", name);
};
  1. dragover: Se ejecuta continuamente mientras un elemento arrastrado pasa sobre un área donde se puede soltar. Llamamos a e.preventDefault() para permitir que el elemento se suelte en el contenedor.
const onDragOver = (e) => {
    e.preventDefault();
};
  1. drop: Se activa cuando el usuario suelta el elemento en un área válida. En onDrop, recuperamos el nombre de la tarea desde dataTransfer y actualizamos el estado para cambiar la categoría de la tarea.
const onDrop = (e, cat) => {
    const name = e.dataTransfer.getData("text/plain");
    setTasks(
        tasks.map((task) =>
            task.name === name ? { ...task, category: cat } : task
        )
    );
};

Estos eventos trabajan juntos para crear una experiencia fluida. Cuando el usuario arrastra una tarea, el navegador registra su nombre. Al soltarla en una categoría, el estado se actualiza y la interfaz se re-renderiza para reflejar el cambio.

Mejorando la experiencia de usuario

Para hacer la aplicación más interactiva, podemos añadir retroalimentación visual durante el arrastre. Por ejemplo, ya definimos la clase CSS .dragging para reducir la opacidad del elemento arrastrado. Para activarla, podemos escuchar el evento drag en cada tarea y aplicar la clase dinámicamente.

Modifica el código de las tareas dentro del bucle forEach:

tasks.forEach((t) => {
    tasksByCategory[t.category].push(
        <div
            key={t.name}
            onDragStart={(e) => onDragStart(e, t.name)}
            onDrag={(e) => e.currentTarget.classList.add("dragging")}
            onDragEnd={(e) => e.currentTarget.classList.remove("dragging")}
            draggable
            className="draggable"
            style={{ backgroundColor: t.bgcolor }}
        >
            {t.name}
        </div>
    );
});

Aquí, onDrag agrega la clase dragging mientras el elemento se mueve, y onDragEnd la elimina cuando el arrastre termina. Esto proporciona una señal visual clara al usuario.

Optimizando el rendimiento

En aplicaciones más grandes, renderizar muchas tareas puede afectar el rendimiento. Para optimizar, considera usar useMemo para memoizar el objeto tasksByCategory y evitar cálculos innecesarios durante el renderizado.

import { useState, useMemo } from "react";

const AppDragDropDemo = () => {
    const [tasks, setTasks] = useState([
        { name: "Aprender React", category: "wip", bgcolor: "yellow" },
        { name: "Dominar JavaScript", category: "wip", bgcolor: "pink" },
        {
            name: "Explorar TypeScript",
            category: "complete",
            bgcolor: "skyblue",
        },
    ]);

    const tasksByCategory = useMemo(() => {
        const categorized = { wip: [], complete: [] };
        tasks.forEach((t) => {
            categorized[t.category].push(
                <div
                    key={t.name}
                    onDragStart={(e) => onDragStart(e, t.name)}
                    onDrag={(e) => e.currentTarget.classList.add("dragging")}
                    onDragEnd={(e) =>
                        e.currentTarget.classList.remove("dragging")
                    }
                    draggable
                    className="draggable"
                    style={{ backgroundColor: t.bgcolor }}
                >
                    {t.name}
                </div>
            );
        });
        return categorized;
    }, [tasks]);

    // Resto del componente...
};

El hook useMemo asegura que tasksByCategory solo se recalcule cuando el arreglo tasks cambie, mejorando la eficiencia en aplicaciones con muchas tareas.

Agregando soporte para dispositivos táctiles

La API de HTML5 Drag and Drop no es compatible de forma nativa con eventos táctiles en dispositivos móviles. Para agregar soporte básico, podemos simular el comportamiento usando eventos como touchstart, touchmove y touchend. Sin embargo, esto requiere una lógica más compleja, como rastrear las coordenadas del toque y determinar el área de destino.

Como ejemplo simplificado, podemos agregar un manejador de touchstart para iniciar el arrastre en dispositivos móviles:

const onTouchStart = (e, name) => {
    e.preventDefault();
    e.currentTarget.classList.add("dragging");
    e.currentTarget.dataset.name = name; // Almacenar el nombre temporalmente
};

tasks.forEach((t) => {
    tasksByCategory[t.category].push(
        <div
            key={t.name}
            onDragStart={(e) => onDragStart(e, t.name)}
            onDrag={(e) => e.currentTarget.classList.add("dragging")}
            onDragEnd={(e) => e.currentTarget.classList.remove("dragging")}
            onTouchStart={(e) => onTouchStart(e, t.name)}
            draggable
            className="draggable"
            style={{ backgroundColor: t.bgcolor }}
        >
            {t.name}
        </div>
    );
});

Este es un punto de partida, pero para una implementación completa en dispositivos móviles, podrías necesitar una librería como react-use-gesture o implementar una solución personalizada más robusta. Para mantener el enfoque en la API nativa, dejaremos esta funcionalidad como opcional.

Manejo de errores y validaciones

Para hacer la aplicación más robusta, podemos agregar validaciones. Por ejemplo, podemos evitar que una tarea se mueva a su propia categoría o manejar casos donde dataTransfer no contiene datos válidos.

Modifica la función onDrop para incluir una validación:

const onDrop = (e, cat) => {
    const name = e.dataTransfer.getData("text/plain");
    if (!name) return; // Evitar errores si no hay datos
    setTasks(
        tasks.map((task) =>
            task.name === name && task.category !== cat
                ? { ...task, category: cat }
                : task
        )
    );
};

Esta versión verifica que name exista y solo actualiza la categoría si es diferente a la actual, evitando actualizaciones innecesarias del estado.

Personalización y escalabilidad

Una vez que la funcionalidad básica está implementada, puedes personalizar la aplicación según tus necesidades. Algunas ideas incluyen:

  • Agregar animaciones de transición usando CSS o una librería como Framer Motion.
  • Implementar un sistema de persistencia, como guardar las tareas en localStorage o una base de datos.
  • Permitir a los usuarios crear, editar o eliminar tareas dinámicamente.
  • Añadir soporte para múltiples tableros o categorías configurables.

Por ejemplo, para agregar una nueva tarea, puedes incluir un formulario en el componente:

const [newTask, setNewTask] = useState("");

const addTask = (e) => {
    e.preventDefault();
    if (!newTask) return;
    setTasks([
        ...tasks,
        { name: newTask, category: "wip", bgcolor: "lightgreen" },
    ]);
    setNewTask("");
};

return (
    <div className="container-drag">
        <h2>DEMO DE ARRASTRAR Y SOLTAR</h2>
        <form onSubmit={addTask}>
            <input
                type="text"
                value={newTask}
                onChange={(e) => setNewTask(e.target.value)}
                placeholder="Nueva tarea"
            />
            <button type="submit">Agregar</button>
        </form>
        {/* Resto del renderizado */}
    </div>
);

Este código agrega un formulario para crear nuevas tareas, asignándolas a la categoría “En progreso” con un color de fondo predefinido.

Conclusiones

Implementar la funcionalidad de arrastrar y soltar en ReactJS sin librerías externas es un proceso accesible que combina la API de HTML5 Drag and Drop con la gestión de estado de React. A lo largo de este tutorial, construimos una aplicación de demostración que permite mover tareas entre categorías, optimizando el rendimiento y añadiendo retroalimentación visual para mejorar la experiencia de usuario. Aunque el enfoque sin librerías ofrece un control total, también tiene limitaciones, como la falta de soporte nativo para dispositivos táctiles. Para proyectos más complejos, podrías considerar librerías como react-beautiful-dnd o dnd-kit, pero la solución presentada es ideal para aplicaciones simples o para aprender los fundamentos de la API. Experimenta con las personalizaciones sugeridas, como agregar formularios o animaciones, para adaptar la funcionalidad a tus necesidades específicas. Con este conocimiento, estás listo para integrar arrastrar y soltar en tus proyectos de React, creando interfaces más interactivas y dinámicas.