DEV Community 👩‍💻👨‍💻

Cover image for ¡Creando un app que usa Drag and Drop con React sin librerías 👆!
Franklin Martinez
Franklin Martinez

Posted on

¡Creando un app que usa Drag and Drop con React sin librerías 👆!

Las aplicaciones que usan drag and drop son muy comunes hoy en día, son excelentes para la experiencia de usuario dentro de un app. Y probablemente te gustaría implementarlo en tu próximo proyecto.

En esta ocasión, te enseñare como realizar una aplicación que tenga la funcionalidad de drag & drop, pero sin usar alguna librería externa, solamente con React JS.

🚨 Nota: Este post requiere que sepas las bases de React con TypeScript (hooks básicos y custom hooks).

Cualquier tipo de Feedback es bienvenido, gracias y espero disfrutes el articulo.🤗

 

Tabla de contenido.

📌 Tecnologías a utilizar.

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Creando nuestras tarjetas.

📌 Creando los contenedores para nuestras tarjetas.

📌 Definiendo el tipo e interfaz para la información de las tarjetas.

📌 Creando el componente DragAndDrop.tsx

📌 Agregando algunos datos para crear tarjetas.

📌 Mostrando algunas tarjetas.

📌 Realizando la funcional de Drag.

📌 Realizando la funcional de Drop.

📌 Creando el estado para mantener las tarjetas.

📌 Realizando las funciones para hacer el drop en los contenedores.

📌 Opcional. Refactorización del código en DragAndDrop.tsx

📌 Conclusión.

📌 Demostración en vivo.

📌 Código fuente.

 

👉 Tecnologías a utilizar.

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

 

👉 Creando el proyecto.

Al proyecto le colocaremos el nombre de: dnd-app (opcional, tu le puedes poner el nombre que gustes).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

cd dnd-app
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

Luego con este comando levantaremos el servidor de desarrollo, y finalmente vamos a un navegador y accedemos a http://localhost:5173 (en vite version 2 el puerto era localhost:3000, pero en la nueva version el puerto es localhost:5173)

npm run dev
Enter fullscreen mode Exit fullscreen mode

 

👉 Primeros pasos.

De una vez, creamos la carpeta src/components y agregamos el archivo Title.tsx y dentro agregamos:

export const Title = () => {
    return (
        <div className="title flex">
            <h1>Creating basic Drag & Drop 👆 </h1>
            <span>( without external libraries )</span>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora, dentro del archivo src/App.tsx borramos todo el contenido del archivo y colocamos un componente funcional que muestre el titulo que acabamos de crear.

import { Title } from "./components/Title"

const App = () => {
  return (
    <div className="container-main flex">
        <Title />
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Debería de verse así 👀:

Title

 

👉 Creando nuestras tarjetas.

Dentro de la carpeta src/components agregamos el archivo CardItem.tsx

Por el momento no recibirá ninguna prop, lo hará después.


export const CardItem = () => {
    return (
        <div className='card-container'>
            <p>content</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Aun NO usaremos el componente Card en un archivo, pero si quieres puedes importarlo en el archivo src/App.tsx para que puedas darle algunos estilos y verlos en pantalla.

 

👉 Creando los contenedores para nuestras tarjetas.

Ahora vamos a crear nuestro contenedor para las tarjetas.
Dentro de la carpeta src/components agregamos el archivo ContainerCards.tsx y agregamos lo siguiente:

Este componente por el momento recibe como parámetro el estado ( puedes ver de que tipo es el Status)

import { Status } from '../interfaces'

interface Props {
  status: Status
}

export const ContainerCards = ({ status }: Props) => {

    return (
        <div className="layout-cards" >
            <p>{status} hero</p>
            {/* Cards */}
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

 

🟠 Definiendo el tipo e interfaz para la información de las tarjetas.

El type Status es el siguiente:

export type Status = 'good' | 'bad' | 'normal'
Enter fullscreen mode Exit fullscreen mode

Este tipo esta en dentro de la carpeta src/interfaces dentro de un archivo index.ts (el cual deben ir creando, ya que el type Status lo usaremos en varios archivos )

Aprovechando que se están creando el index.ts en src/interfaces también agreguen la siguiente interfaz.

Asi es como lucirán los datos de las tarjetas.

export interface Data {
    id: number
    content: string
    status: Status
}
Enter fullscreen mode Exit fullscreen mode

 

👉 Creando el componente DragAndDrop.tsx

Bueno, hasta aquí ya hemos creado el componente que contendrá las tarjetas, pero necesitamos 3 contenedores de tarjetas:

  • Uno para lo heroes buenos.
  • Uno para lo heroes normales.
  • Uno para lo heroes malos.

Dentro de la carpeta src/components agregamos el archivo DragAndDrop.tsx y agregamos lo siguiente:

import { Status } from "../interfaces"
import { ContainerCards } from "./ContainerCards"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {
    return (
        <div className="grid">
            {
                typesHero.map( container => (
                    <ContainerCards
                        status={container}
                        key={container}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Este componente debemos agregarlo al src/App.tsx

import { DragAndDrop} from "./components/DragAndDrop"
import { Title } from "./components/Title"

const App = () => {

  return (
    <div className="container-main flex">
      <Title />
      <DragAndDrop />
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Debería de verse algo como esto por el momento 👀...
Grid without cards

Listo ya tenemos los contenedores donde se podrán soltar las tarjetas y clasificarlas. 👋

Ahora necesitamos crear algunas tarjetas.

 

👉 Agregando algunos datos para crear tarjetas.

Ahora creamos una carpeta src/assets y dentro un archivo index.ts el cual contendrá un listado con datos para rellenar las tarjetas.

import { Data } from "../interfaces";

export const data: Data[] = [
    {
        id: 1,
        content: 'Aqua-man',
        status: 'good'
    },
    {
        id: 2,
        content: 'Flash',
        status: 'normal'
    },
    {
        id: 3,
        content: 'Green Lantern',
        status: 'good'
    },
    {
        id: 4,
        content: 'Batman',
        status: 'bad'
    },
]

Enter fullscreen mode Exit fullscreen mode

Ahora, devuelta en src/componentes/DragAndDrop.tsx en componente ContainerCards le pasamos una nueva prop llamada items a dicha prop le pasamos como valor la data que hemos creado en la carpeta src/assets

import { ContainerCards } from "./ContainerCards"
import { Status } from "../interfaces"
import { data } from "../assets"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {
    return (
        <div className="grid">
            {
                typesHero.map( container => (
                    <ContainerCards
                        status={container}
                        key={container}
                        items={data}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Esto nos marcara error ya que items no es una propiedad que ContainerCards este esperando. 😥

Pero eso lo arreglamos en la siguiente sección. 👇

 

👉 Mostrando algunas tarjetas.

Para mostrar algunas tarjetas, necesitamos hacer unos cambios en los parámetros de cada componente.

1 - Primero el componente src/components/CardItem.tsx

Recibirá como props la data que es de tipo Data, la que habíamos definido con anterioridad .

De una vez mostramos la propiedad content dentro de data.

import { Data } from "../interfaces"

interface Props {
    data: Data
}

export const CardItem = ({ data, handleDragging }: Props) => {

    return (
        <div className='card-container'>
            <p>{data.content}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

2 - En el componente src/components/ContainerCards.tsx cambiamos las interfaz de Props agregando la propiedad items que es un listado de Data y la desestructuramos en el componente

import { Data, Status } from "../interfaces"

interface Props {
    items: Data[]
    status: Status
}

export const ContainerCards = ({ items = [], status }: Props) => {

    return (
        <div className="layout-cards">
            <p>{status} hero</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Luego debajo de la etiqueta p realizamos una iteración a los items.
Y retornamos la el CardItem.tsx mandando el item a la propiedad de data del CardItem

import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"

interface Props {
    items: Data[]
    status: Status
}

export const ContainerCards = ({ items = [], status}: Props) => {

    return (
        <div className="layout-cards">
            <p>{status} hero</p>
            {
                items.map(item => (
                    <CardItem
                        data={item}
                        key={item.id}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Esto te dará una advertencia de que las key se repiten 😥

Esto es debido a que estamos renderizando 3 veces el ContainerCards.

Pero espera la única propiedad que hará la diferencia entre estos 3 componentes es el status

Por lo que haremos la siguiente condición:

  • Si el estado que recibe el componente ContainerCards es igual al estado del item (o sea del super héroe) entonces renderizalo, de lo contrario retorna falso.
import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"

interface Props {
    items: Data[]
    status: Status
}

export const ContainerCards = ({ items = [], status }: Props) => {

    return (
        <div className="layout-cards">
            <p>{status} hero</p>
            {
                items.map(item => (
                    status === item.status
                    && <CardItem
                        data={item}
                        key={item.id}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Y asi evitamos el conflicto con las llaves y se clasificaran las tarjetas de la siguiente manera 👀...

Grid and cards

 

👉 Realizando la funcional de Drag.

Para realizar la funcionalidad de drag, primero vamos a definir un estado y una función en src/components/DragAndDrop.tsx

  • El estado nos ayudara a saber si si esta haciendo drag, y asi cambiar los estilos de.

    • Y por defecto sera false, ya que al inicio de la aplicación no se estará haciendo haciendo drag.
    • Solo sera true cuando se arrastre alguna tarjeta.
  • La función, la cual recibe un valor booleano, nos ayudara a cambiar al valor al estado, esto lo hado para no pasar el setter setIsDragging como prop.

Pasamos como prop al componente ContainerCards:

  • isDragging, tendrá el valor del estado
  • handleDragging, sera la función que creamos para actualizar el estado.
import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {

  const [isDragging, setIsDragging] = useState(false)

  const handleDragging = (dragging: boolean) => setIsDragging(dragging)


    return (
        <div className="grid">
            {
                typesHero.map(container => (
                    <ContainerCards
                        items={data}
                        status={container}
                        key={container}

                        isDragging={isDragging}
                        handleDragging={handleDragging}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Esto marcara error porque ContainerCards no espera esas propiedades.

Asi que vamos a tener que cambiar la interfaz de ContainerCards
El el archivo src/components/ContainerCards.tsx


interface Props {
    items: Data[]
    status: Status
    isDragging: boolean
    handleDragging: (dragging: boolean) => void
}
Enter fullscreen mode Exit fullscreen mode

Y de una vez obtenemos esas props.

  • Nota que en el className del div colocamos una condición, donde si isDragging es verdadero entonces agregamos la clase layout-dragging. Esta clase solo cambiara el color de fondo y el borde del contenedor, cuando se arrastre una tarjeta.

  • Nota, que también pasamos una nueva prop al CardItem la cual es handleDragging, esto es porque la tarjeta es el componente que va actualizar el estado que creamos con anterioridad.

import { CardItem } from "./CardItem"
import { Data, Status } from "../interfaces"

interface Props {
    items: Data[]
    status: Status
    isDragging: boolean
    handleDragging: (dragging: boolean) => void
}

export const ContainerCards = ({ items = [], status, isDragging, handleDragging }: Props) => {

    return (
        <div
            className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
        >
            <p>{status} hero</p>
            {
                items.map(item => (
                    status === item.status
                    && <CardItem
                        data={item}
                        key={item.id}
                        handleDragging={handleDragging}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

El CardItem nos marcara error ya que no espera la propiedad handleDragging, por lo que debemos modificar su interfaz.

Ahora en el archivo src/components/CardItem.tsx modificamos la interfaz

interface Props {
    data: Data,
    handleDragging: (dragging: boolean) => void
}
Enter fullscreen mode Exit fullscreen mode

Y ahora si, empezamos a agregar la funcionalidad de drag en este componente.
Primero al div que es el toda la tarjeta, le agregamos el atributo draggable para indicar que este componente se puede arrastrar.

import { Data } from "../interfaces"

interface Props {
    data: Data,
    handleDragging: (dragging: boolean) => void
}

export const CardItem = ({ data, handleDragging }: Props) => {
    return (
        <div
            className='card-container'
            draggable
        >
            <p>{data.content}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Luego agregamos el atributo onDragEnd que va a ejecutar la función handleDragEnd.

Dicha función lo único que hará es colocar el valor del estado isDragging en false, porque cuando se ejecute onDragEnd ya se habrá dejado de arrastrar la tarjeta por lo que tenemos que quitar los estilos de cuando se hace drag, o sea volver todos los estilos como al inicio.

import { Data } from "../interfaces"

interface Props {
    data: Data,
    handleDragging: (dragging: boolean) => void
}

export const CardItem = ({ data, handleDragging }: Props) => {


    const handleDragEnd = () => handleDragging(false)

    return (
        <div
            className='card-container'
            draggable
            onDragEnd={handleDragEnd}
        >
            <p>{data.content}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Luego agregamos el atributo onDragStart (se ejecuta cuando se empieza arrastrar el componente, si no le colocáramos el atributo draggable, entonces onDragStart no se ejecutaría).

onDragStart va ejecutar la función handleDragStart.

Esta función recibe el evento y dentro del evento hay una propiedad que nos interesa que es la de dataTransfer.

La propiedad dataTransfer nos permite contener u obtener datos cuando se esta arrastrando un elemento.

La propiedad setData dentro de dataTransfer, establece los datos que queremos contener al momento de arrastrar un elemento, y recibe dos parámetros:

  • format: es el formato de la data a mantener, el cual es "text"

  • data: es la información que queremos contener mientras se hace el arrastre del elemento. Solo acepta un string. En este caso, almacenaremos el id de la tarjeta.

NOTA: también existe una propiedad dentro de dataTransfer llamada clearData que limpia el cache de los datos que almacenamos. En este caso no es necesario ejecutarlo, ya que vamos a estar sobrescribiendo el mismo identificador 'text'.

Después de contener la data, ejecutamos handleDragging mandando el valor de true para indicar al usuario que estamos arrastrando un elemento.

import { Data } from "../interfaces"

interface Props {
    data: Data,
    handleDragging: (dragging: boolean) => void
}

export const CardItem = ({ data, handleDragging }: Props) => {

    const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
        e.dataTransfer.setData('text', `${data.id}`)
        handleDragging(true)
    }
    const handleDragEnd = () => handleDragging(false)

    return (
        <div
            className='card-container'
            draggable
            onDragStart={handleDragStart}
            onDragEnd={handleDragEnd}
        >
            <p>{data.content}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Y asi tendríamos la parte de arrastrar un elemento, ya tendríamos la información contenida lista para obtenerla cuando se suelte en otro contenedor.

Asi se vería cuando arrastramos una tarjeta, cambia el diseño de los contenedores indicando que son los lugares donde puedes soltar la tarjeta.

Grid dragging effect

 

👉 Realizando la funcional de Drop.

Antes de hacer la parte de soltar el elemento, debemos realizar otras cosas antes.

🟠 Creando el estado para mantener las tarjetas.

Primero establecer la lista de heroes en un estado y poder actualizarla cuando se suelte la tarjeta en otro contenedor,en ese momento actualizaríamos la propiedad status del héroe, lo que provocara que se vuelva a renderizar de nuevo el listado organizando las tarjetas que cambiaron.

Para eso vamos a src/components/DragAndDrop.tsx y creamos un nuevo estado.
Su valor inicial va a ser la data que hemos definido previamente en src/assets.

import { data } from "../assets"

const [listItems, setListItems] = useState<Data[]>(data)
Enter fullscreen mode Exit fullscreen mode

Y ahora, al momento de renderizar el componente ContainerCards, en vez de pasar el valor de data a la prop de items, le mandaremos el valor del estado listItems.

import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {

  const [isDragging, setIsDragging] = useState(false)
  const [listItems, setListItems] = useState<Data[]>(data)

  const handleDragging = (dragging: boolean) => setIsDragging(dragging)


    return (
        <div className="grid">
            {
                typesHero.map(container => (
                    <ContainerCards
                        items={listItems}
                        status={container}
                        key={container}

                        isDragging={isDragging}
                        handleDragging={handleDragging}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Después crearemos una función para actualizar el estado de la listItems.
La llamaremos handleUpdateList, y recibirá dos parámetros:

  • id: el identificador de la tarjeta, sera de tipo número.
  • status: el nuevo estado de la tarjeta, sera de tipo Status.

Dentro de la función ...

1 - Primero buscaremos el elemento en el valor del estado listItems, mediante el ID.

2 - Evaluaremos si los datos existen y si el status que nos pasan es diferente al status que ya tiene, entonces haremos los cambios en el estado.

3 - Dentro de la condición, accedemos a la tarjeta encontrada y actualizaremos su propiedad status asignándole el nuevo status que nos llega por parámetro en la función.

4 - Llamamos al setListItems para actualizar el estado, colocando:

  • La tarjeta con su propiedad status actualizada.

  • Un nuevo arreglo, filtrando los elementos para quitar el tarjeta que estamos actualizando y evitar que se duplique la información.

Ahora, al componente ContainerCards le agregamos una nueva propiedad llamada handleUpdateList y le mandamos la función que acabamos de crear handleUpdateList.

import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {

  const [isDragging, setIsDragging] = useState(false)
  const [listItems, setListItems] = useState<Data[]>(data)

  const handleDragging = (dragging: boolean) => setIsDragging(dragging)

  const handleUpdateList = (id: number, status: Status) => {

       let card = listItems.find(item => item.id === id)

       if (card && card.status !== status) {

           card.status = status

           setListItems( prev => ([
                card!,
                ...prev.filter(item => item.id !== id)
            ]))
       }
   }

    return (
        <div className="grid">
            {
                typesHero.map(container => (
                    <ContainerCards
                        items={listItems}
                        status={container}
                        key={container}

                        isDragging={isDragging}
                        handleDragging={handleDragging}
                        handleUpdateList={handleUpdateList}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Esto nos marcara error, porque el componente ContainerCards no espera la propiedad handleUpdateList, asi que debemos actualizar la interfaz de ContainerCards.

En src/components/ContainerCards.tsx:

interface Props {
    items: Data[]
    status: Status
    isDragging: boolean
    handleDragging: (dragging: boolean) => void
    handleUpdateList: (id: number, status: Status) => void
}
Enter fullscreen mode Exit fullscreen mode

 

👉 Realizando las funciones para hacer el drop en los contenedores.

Estamos en src/components/ContainerCards.tsx.

Dentro del componente vamos a establecer dos propiedades nuevas al elemento div.

  • onDragOver: se produce cuando un elemento que es arrastrable se arrastra sobre un objetivo de soltar valido. Le pasamos la función handleDragOver, que crearemos en un instante.

  • onDrop: se produce cuando el elemento arrastrado se deja caer. Le pasamos la función handleDrop, que crearemos en un instante.

<div
    className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
    onDragOver={handleDragOver}
    onDrop={handleDrop}
>
    <p>{status} hero</p>
    {
        items.map(item => (
            status === item.status
            && <CardItem
                data={item}
                key={item.id}
                handleDragging={handleDragging}
            />
        ))
    }
</div>
Enter fullscreen mode Exit fullscreen mode

La función handleDragOver solo hará esto.
Primero, recibirá el evento que emite onDragOver.

Ya que por defecto los datos no pueden ser soltados en otros elementos y para permitir soltarlos debemos evitar el comportamiento por defecto.

const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault()
}
Enter fullscreen mode Exit fullscreen mode

Ahora la función handleDrop

  • Primero, recibirá el evento que emite onDrop.

  • Dentro de la función, evitamos el comportamiento por defecto, el cual se nota mas con imágenes (cuando soltamos una imagen en un lugar de nuestra app, abre la imagen, sacándonos de la app).

  • Entonces, del evento, obtenemos la propiedad dataTransfer y mediante la propiedad de getData de dataTransfer, la ejecutamos mandando el identificador del cual obtendremos el ID de la tarjeta.

    • El signo de + al inicio de e.dataTransfer.getData('text') es para convertir el valor a un número.
  • Luego llamaremos la función handleUpdateList que el componente nos pasa por props, (hay que desestructurarlo del componente).

    • Le pasamos primero el id que obtuvimos de la propiedad getData de dataTransfer ya convertido en número.
    • Después le pasamos el status que recibimos por props en el componente.
  • Finalmente llamamos handleDragging mandando el valor de false para indicar al usuario que ya no estamos arrastrando nada.

const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault()
    const id = +e.dataTransfer.getData('text')
    handleUpdateList(id, status)
    handleDragging(false)
}
Enter fullscreen mode Exit fullscreen mode

Así se vería el código de src/components/ContainerCards.tsx

import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"

interface Props {
    items: Data[]
    status: Status
    isDragging: boolean
    handleUpdateList: (id: number, status: Status) => void
    handleDragging: (dragging: boolean) => void
}

export const ContainerCards = ({ items = [], status, isDragging, handleDragging, handleUpdateList }: Props) => {

    const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault()
        handleUpdateList(+e.dataTransfer.getData('text'), status)
        handleDragging(false)
    }

    const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => e.preventDefault()

    return (
        <div
            className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
            onDrop={handleDrop}
            onDragOver={handleDragOver}
        >
            <p>{status} hero</p>
            {
                items.map(item => (
                    status === item.status
                    && <CardItem
                        data={item}
                        key={item.id}
                        handleDragging={handleDragging}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

El resultado final debería verse de esta manera 🥳!

App demo gif

 

👉 Opcional. Refactorización del código en DragAndDrop.tsx

Tenemos bastante lógica en nuestro componente, por lo cual seria una buena opción crear un custom hook para administrar esa lógica.

Creamos una carpeta de src/hooks y dentro un archivo llamado useDragAndDrop.ts

Primero definimos la función, la cual recibirá un estado inicial que sera de tipo arreglo de Data

export const useDragAndDrop = (initialState: Data[]) => {}
Enter fullscreen mode Exit fullscreen mode

Del componente DragAndDrop.tsx recortamos toda la lógica y la colocamos en el custom hook.

El valor inicial del estado de listItems sera que nos pasen por parámetro del hook.

Y por ultimo retornamos como un objeto:

  • isDragging.
  • listItems.
  • handleUpdateList.
  • handleDragging.
import { useState } from "react"
import { Data, Status } from "../interfaces"

export const useDragAndDrop = (initialState: Data[]) => {

    const [isDragging, setIsDragging] = useState(false)
    const [listItems, setListItems] = useState<Data[]>(initialState)

    const handleUpdateList = (id: number, status: Status) => {

       let card = listItems.find(item => item.id === id)

       if (card && card.status !== status) {

           card.status = status

           setListItems( prev => ([
                card!,
                ...prev.filter(item => item.id !== id)
            ]))
       }
   }

    const handleDragging = (dragging: boolean) => setIsDragging(dragging)

    return {
        isDragging,
        listItems,
        handleUpdateList,
        handleDragging,
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora en el componente src/components/DragAndDrop.tsx llamamos a nuestro custom hook.

Le mandamos la data a nuestro hook, por parámetro y solo desestructuramos las propiedades y listo!.

import { ContainerCards } from "./ContainerCards"
import { useDragAndDrop } from "../hooks/useDragAndDrop"
import { Status } from "../interfaces"
import { data } from "../assets"

const typesHero: Status[] = ['good', 'normal', 'bad']

export const DragAndDrop = () => {

    const { isDragging, listItems, handleDragging, handleUpdateList } = useDragAndDrop(data)

    return (
        <div className="grid">
            {
                typesHero.map(container => (
                    <ContainerCards
                        items={listItems}
                        status={container}
                        key={container}

                        isDragging={isDragging}
                        handleDragging={handleDragging}
                        handleUpdateList={handleUpdateList}
                    />
                ))
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Así quedara mas legible tu componente. 🎉

 

👉 Conclusión.

Este proceso es una de las formas de construir una aplicación con funcionalidad de Drag & Drop sin usar librerías externas.

  • Una forma de mejorar esta aplicación seria usando un administrador de estado para evitar estar pasando demasiadas props a los componentes.

  • Si quieres algo mas elaborado y expandir las funcionalidades, puedes optar por un paquete de terceros que te recomiendo bastante, y es react-beautiful-dnd, una librería muy buena y popular.

Espero haberte ayudado a entender como realizar este ejercicio,muchas gracias por llegar hasta aquí! 🤗❤️

Te invito a que comentes si es que este articulo te resulta útil o interesante, o si es que conoces alguna otra forma distinta o mejor de como hacer un drag & drop. 🙌

 

🟠 Demostración en vivo.

https://drag-and-drop-react-app.netlify.app

🟠 Código fuente.

GitHub logo Franklin361 / drag-and-drop-react

Creating an application using Drag & Drop with React JS 🤏

Creating an app using Drag and Drop with React without libraries 👆!

This time, we are going to implement the functionality to do a Drag & Drop with React JS and without any other external package or library!

 

App demo gif

 

Features ⚙️

  1. Card dragging.
  2. Dropping cards into a container.
  3. Sorting cards.

 

Technologies 🧪

  • React JS
  • TypeScript
  • Vite JS
  • Vanilla CSS 3

 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/drag-and-drop-react
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies of the project.
    npm install
Enter fullscreen mode Exit fullscreen mode
  1. Run the project.
    npm run dev
Enter fullscreen mode Exit fullscreen mode

 

Links ⛓️

Demo of the application 🔥

Here's the link to the tutorial in case you'd like to take a look at it! eyes 👀

Top comments (1)

Collapse
 
franklin030601 profile image
Franklin Martinez Author

Generalmente publico cada viernes, pero a partir de hoy tomare un descanso de dos semanas ✈️ por lo que no publicare nada en ese lapso de tiempo 😔, buen día! ❤️

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.