🚚 react-dnd Cheatsheet Completo 🚚

react-dnd es un conjunto de utilidades para crear interfaces de usuario de arrastrar y soltar en React. Se basa en la API HTML5 de arrastrar y soltar (o en backends personalizados) y se centra en mantener el estado de arrastrar/soltar fuera de tus componentes, lo que los hace fáciles de probar y componer.


1. 🌟 Conceptos Clave


2. 🛠️ Configuración Inicial

  1. Instalación:

    npm install react-dnd react-dnd-html5-backend
    # o
    yarn add react-dnd react-dnd-html5-backend
  2. Envolver la Aplicación con DndProvider:

    • Esto se hace típicamente en el archivo raíz de tu aplicación (ej. App.js, index.js, main.jsx).
    // src/App.js
    import React from 'react';
    import { DndProvider } from 'react-dnd';
    import { HTML5Backend } from 'react-dnd-html5-backend'; // O TouchBackend
    
    import MyDragAndDropApp from './MyDragAndDropApp'; // Tu componente principal con D&D
    
    function App() {
      return (
        <DndProvider backend={HTML5Backend}> {/* Provee el backend a toda la aplicación */}
          <MyDragAndDropApp />
        </DndProvider>
      );
    }
    
    export default App;

3. 🚀 Componentes Principales: useDrag y useDrop Hooks

Estos son los hooks fundamentales para implementar arrastrar y soltar.

3.1. useDrag() Hook (Para el Origen de Arrastre)

Hace que un componente sea arrastrable.

3.2. useDrop() Hook (Para el Objetivo de Soltar)

Hace que un componente sea un objetivo de soltar.


4. 🚀 Ejemplo Básico (Arrastrar un Cuadro a una Papelera)

// src/components/Box.js
import React from 'react';
import { useDrag } from 'react-dnd';

const ItemTypes = {
  BOX: 'box',
};

function Box({ id, name }) {
  const [{ isDragging }, dragRef] = useDrag(() =&gt; ({
    type: ItemTypes.BOX,
    item: { id, name }, // Datos que se envían al soltar
    collect: (monitor) =&gt; ({
      isDragging: monitor.isDragging(), // True si este box está siendo arrastrado
    }),
  }));

  const style = {
    padding: '10px',
    margin: '10px',
    backgroundColor: isDragging ? 'lightgray' : 'white',
    border: '1px solid black',
    cursor: 'move',
    opacity: isDragging ? 0.5 : 1, // Reducir opacidad al arrastrar
  };

  return (
    <div ref={dragRef} style={style}>
      {name}
    </div>
  );
}
export default Box;

// src/components/Dustbin.js
import React from 'react';
import { useDrop } from 'react-dnd';

const ItemTypes = {
  BOX: 'box',
};

function Dustbin() {
  const [{ canDrop, isOver }, dropRef] = useDrop(() =&gt; ({
    accept: ItemTypes.BOX, // Acepta items de tipo BOX
    collect: (monitor) =&gt; ({
      isOver: monitor.isOver(),   // True si un item está sobre la papelera
      canDrop: monitor.canDrop(), // True si el item actual puede ser soltado
    }),
    drop: (item, monitor) =&gt; { // Se llama al soltar
      console.log('Item soltado en la papelera:', item);
      alert(`¡"${item.name}" eliminado!`);
      // Aquí puedes dispatch una acción para eliminar el item del estado global
      return { dropped: true, itemId: item.id }; // Resultado que se puede recuperar en end() del DragSource
    },
  }));

  const isActive = canDrop && isOver;
  let backgroundColor = '#f0f0f0';
  if (isActive) {
    backgroundColor = '#aaddaa'; // Verde si se puede soltar
  } else if (canDrop) {
    backgroundColor = '#ffffaa'; // Amarillo si se puede soltar pero no está encima
  }

  const style = {
    width: '200px',
    height: '100px',
    border: '1px dashed black',
    backgroundColor,
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    margin: '20px',
  };

  return (
    <div ref={dropRef} style={style}>
      {isActive ? '¡Suelta aquí!' : 'Arrastra un cuadro aquí'}
    </div>
  );
}
export default Dustbin;

// src/MyDragAndDropApp.js
import React from 'react';
import Box from './components/Box';
import Dustbin from './components/Dustbin';

function MyDragAndDropApp() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
      <h1>Arrastrar y Soltar Simple</h1>
      <Box id="box1" name="Mi Primer Cuadro" />
      <Box id="box2" name="Otro Cuadro" />
      <Dustbin />
    </div>
  );
}
export default MyDragAndDropApp;

5. 🎨 Personalización de la Vista Previa de Arrastre

Por defecto, el navegador crea una “imagen fantasma” del elemento arrastrado. Puedes personalizarla.

5.1. DragPreviewImage

import React, { useEffect } from 'react';
import { useDrag, DragPreviewImage } from 'react-dnd';

function CustomDragPreviewBox({ id, name, imageUrl }) {
  const [{ isDragging }, dragRef, preview] = useDrag(() =&gt; ({
    type: 'CUSTOM_BOX',
    item: { id, name },
    collect: (monitor) =&gt; ({
      isDragging: monitor.isDragging(),
    }),
  }));

  // Usa useEffect para vincular la imagen de vista previa
  // preview es la función que se pasa como segundo elemento del array de retorno de useDrag
  useEffect(() =&gt; {
    if (preview) {
      const img = new Image();
      img.src = imageUrl;
      img.onload = () =&gt; preview(img); // Registra la imagen como vista previa
    }
  }, [preview, imageUrl]);

  return (
    &lt;>
      {/* Opcional: <DragPreviewImage connect={preview} src={imageUrl} /> (más simple para una imagen directa) */}
      <div ref={dragRef} style={{ opacity: isDragging ? 0.5 : 1 }}>
        {name} (Arrastrarme con imagen)
      </div>
    </>
  );
}
export default CustomDragPreviewBox;

6. 🔄 Reordenar Elementos en una Lista

Este es un caso de uso común y más complejo, que involucra hover en useDrop y la lógica de mutación en el estado del componente padre.

Principios:

  1. Cada elemento de la lista es un DragSource y un DropTarget.
  2. El hover del DropTarget maneja la reordenación visual (movimiento del placeholder).
  3. El drop del DropTarget dispara la acción de actualización real del estado en el componente padre.
// src/components/DraggableCard.js
import React, { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';

const ItemTypes = { CARD: 'card' };

function DraggableCard({ id, text, index, moveCard }) {
  const ref = useRef(null);

  // useDrag: para hacer la tarjeta arrastrable
  const [{ isDragging }, drag] = useDrag(() =&gt; ({
    type: ItemTypes.CARD,
    item: { id, index },
    collect: (monitor) =&gt; ({
      isDragging: monitor.isDragging(),
    }),
  }));

  // useDrop: para hacer la tarjeta un objetivo donde soltar
  const [, drop] = useDrop(() =&gt; ({
    accept: ItemTypes.CARD,
    hover: (draggedItem, monitor) =&gt; { // Se ejecuta cuando un item está sobre este
      if (!ref.current) return;
      if (draggedItem.id === id) return; // No mover si es el mismo item

      const hoverBoundingRect = ref.current?.getBoundingClientRect();
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;

      // Solo mover si el item arrastrado ha pasado el "punto medio" del item hovereado
      if (draggedItem.index &lt; index && hoverClientY &lt; hoverMiddleY) return;
      if (draggedItem.index > index && hoverClientY > hoverMiddleY) return;

      moveCard(draggedItem.id, id); // Llama a la función del padre para reordenar
      draggedItem.index = index; // Actualiza el índice del item arrastrado para el siguiente hover
    },
  }));

  drag(drop(ref)); // Conecta ambas refs al mismo nodo DOM

  const opacity = isDragging ? 0 : 1;
  const style = {
    padding: '8px 12px',
    margin: '4px',
    backgroundColor: 'white',
    border: '1px solid #ccc',
    cursor: 'grab',
    opacity,
  };

  return (
    <div ref={ref} style={style}>
      {text}
    </div>
  );
}
export default DraggableCard;

// src/components/CardList.js
import React, { useState, useCallback } from 'react';
import update from 'immutability-helper'; // npm install immutability-helper
import DraggableCard from './DraggableCard';

function CardList() {
  const [cards, setCards] = useState([
    { id: 1, text: 'Aprender React' },
    { id: 2, text: 'Estudiar Vue' },
    { id: 3, text: 'Explorar Angular' },
    { id: 4, text: 'Dominar Node.js' },
  ]);

  // Función para reordenar las tarjetas
  const moveCard = useCallback((draggedId, targetId) =&gt; {
    const draggedIndex = cards.findIndex(card =&gt; card.id === draggedId);
    const targetIndex = cards.findIndex(card =&gt; card.id === targetId);
    const draggedCard = cards[draggedIndex];

    setCards(
      update(cards, {
        $splice: [
          [draggedIndex, 1], // Eliminar la tarjeta arrastrada
          [targetIndex, 0, draggedCard], // Insertarla en la nueva posición
        ],
      })
    );
  }, [cards]);

  return (
    <div style={{ width: '300px', border: '1px dashed lightgray', padding: '10px' }}>
      <h3>Lista Reordenable</h3>
      {cards.map((card, index) =&gt; (
        <DraggableCard
          key={card.id}
          id={card.id}
          text={card.text}
          index={index}
          moveCard={moveCard}
        />
      ))}
    </div>
  );
}
export default CardList;

// En tu App.js (después de DndProvider)
// import CardList from './components/CardList';
// <CardList />

7. 💡 Buenas Prácticas y Consejos


Este cheatsheet te proporciona una referencia completa de react-dnd, cubriendo sus conceptos esenciales, cómo configurar el proveedor, los hooks useDrag y useDrop, ejemplos básicos y avanzados, estilización y las mejores prácticas para construir interfaces de usuario de arrastrar y soltar en React.