Crear una aplicación para subir images - React y Cloudinary ⚛️.
Tags: react, typescript, programming, spanish
Published: 2022-06-09 14:50:04

Esta aplicación consiste en una interfaz donde se podrán subir imágenes mediante Drag & Drop y dicha imagen se guardara en [Cloudinary](https://cloudinary.com/).

**El enlace al código esta al final de este post.**

Índice
- [Primeros pasos](#2).
 1. [Configurando Cloudinary](#3).
 2. [Creando el proyecto con create-react-app](#4).
3. [Creando el componente de titulo](#5).
4. [Creando el componente de Drag & Drop](#6).
5. [Creando el componente de Box Drag & Drop](#7).
6. [Creando el componente de Image Selected](#8).
- [Llenando el componente con funciones y estado](#9).
 1. [Agregando la funcionalidad para subir imágenes a Cloudinary](#10).
8. [Mostrar link de la imagen subida a Cloudinary](#11).
9. [Ocultar link de la imagen después de unos segundos](#12).
10. [Introducción](#13).

--- 

## 🟣 Primeros pasos.<a id="2"></a>
### 🟠 Configurando Cloudinary.<a id="3"></a>

1. Iniciar sesión en Cloudinary o crearte una cuenta.
2. En el Dashboard, te aparcera el nombre de tu nube (deberás guardarlo en un bloc de notas o algo ya que lo usaremos después).
3. Presionar el icono de engrane que te llevara a la configuración.
4. Seleccionar la pestaña upload.
5. Bajar hasta donde dice **“Upload presets:”**
6. Presionar el enlace que dice **“Add upload preset”**
7. Donde dice **“Upload preset name”**, en la caja de texto colocar un nombre para ese preset. (ej: zt1zhk4z, deberás guardarlo en un bloc de notas o algo ya que lo usaremos después)
8. Donde dice **“Signing Mode”** seleccionar **Unsigned**
9. Presionar el botón save (se encuentra en la parte superior de la pagina) para guardar preset.

### 🟠 Creando el proyecto con create-react-app. <a id="4"></a>

Necesitamos crear un nuevo proyecto de React. En este caso lo hare con la herramienta de [create-react-app](https://create-react-app.dev/docs/getting-started) usando TypeScript.

```jsx
npx create-react-app upload-image-app --template typescript
```

Luego de haberse creado nos dirigimos al proyecto lo abrimos con el editor de cogido de preferencia. En mi caso, Visual Studio Code.

```shell
cd upload-image-app && code .
```

Ahora, necesitaremos instalar un paquete de terceros llamado [react-images-uploading](https://www.npmjs.com/package/react-images-uploading), el cual nos ayudara a trabajar la acción de Drag & Drop con las imágenes,

```bash
npm install react-images-uploading
```

## 🟣 Creando el componente de titulo. <a id="5"></a>

Dentro de la carpeta `src/components` creamos el archivo `Title.tsx`. Y agregamos el siguiente código.

```jsx
import React from 'react';

export const Title = () => {
    return (
        <>
        <div className='container_blob'>
            <SVG/>
        </div>
        <h1 className="title">
            <span>Upload image</span><br />
            <span> with</span> <br />
            <span>React & Cloudinary</span>
        </h1>
        </>
    )
}

const SVG = () => {
    return (
        <svg className='svg_blob' viewBox="50 0 100 100" xmlns="http://www.w3.org/2000/svg">
            <path d="M29.9,-47.6C39.2,-34.4,47.5,-26.3,49.9,-16.8C52.2,-7.2,48.5,3.7,44.7,14.3C40.9,24.9,37,35.2,29.5,44.4C22,53.6,11,61.8,-1.3,63.5C-13.6,65.3,-27.1,60.6,-39.3,52.9C-51.5,45.2,-62.2,34.5,-66.6,21.5C-71,8.5,-69,-6.6,-62.9,-18.9C-56.8,-31.1,-46.5,-40.5,-35.3,-53C-24.1,-65.6,-12.1,-81.3,-0.9,-80C10.3,-78.8,20.6,-60.7,29.9,-47.6Z" transform="translate(100 100)" />
        </svg>
    )
}
```

Después nos dirigimos al archivo `src/App.tsx` y borramos todo, para agregar lo siguiente: 

```jsx
import React from 'react';
import { Title } from './components';

const App = () => {
  return (
    <div className="container-grid">
      <Title />
    </div>
  )
}
export default App
```

![Title component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5ou07fo7u0g304hp8px.png)

Para la parte de los estilos, pueden revisar mi código que esta en GitHub, esto lo hago para que el articulo no se haga tan largo y solo concentrarme en la parte importante.

## 🟣 Creando el componente de Drag & Drop. <a id="6"></a>

Dentro de la carpeta `src/components` creamos un archivo llamado `DragAndDrop.tsx`

Primero haremos uso del estado para manejar el comportamiento del componente cuando se seleccione alguna imagen o se arrastre y suelte la imagen dentro del componente.

El componente ImageUploading le colocamos los siguientes propiedades:

- **<u>multiple</u>** → en false, para solo seleccionar una imagen a la vez.
- **<u>maxNumber</u>** → en 1, ya que solo aceptaremos una imagen.
- **<u>value</u>** → un valor de tipo ImageListType. Le pasamos el valor del estado “images”.
- **<u>onChange</u>** → un método que se ejecuta cuando cuando se selecciona una imagen (este método recibe dos parámetros, pero a nosotros solo nos importa el primero, el cual es un array de objetos que contiene la información de la imagen seleccionada). Le pasamos la función handleChange (dicha función actualiza el estado, agregando la imagen seleccionada al estado).

```jsx
import React, { useState } from 'react';
import ImageUploading, { ImageListType } from "react-images-uploading";

export const DragAndDrop = () => {

  const [images, setImages] = useState<ImageListType>([]);
  
  const handleChange = (imageList: ImageListType) => setImages(imageList);

  return (
    <>      
      <ImageUploading multiple={false} maxNumber={1} value={images} onChange={handleChange}>
      </ImageUploading>
    </>
  )
}
```

El componente ImageUploading recibe un función como hijo, dicha función nos da acceso a ciertas parámetros, de los cuales usaremos los siguientes:

- **<u>ImageList</u>** → un valor de tipo ImageListType, que nos trae un arreglo de las imágenes que se han seleccionado (en este caso solo debe ser una imagen seleccionada, por lo cual siempre apuntaremos a la posición 0, ejemplo: `imageList[0]`).
- **<u>dragProps</u>** → es un conjunto de métodos que nos ayudaran a realizar la acción de Drag & Drop.
- **<u>isDragging</u>** → retorna true si se esta arrastrando una imagen al componente, de lo contrario se queda en false.
- **<u>onImageUpload</u>** → Método que al ejecutarse abre el explorador de archivos del dispositivo para seleccionar una imagen.
- **<u>onImageRemove</u>** → Método que recibe un índice de la imagen que se quiere quitar y la remueve de la lista (que en este caso siempre sera el índice 0).<a id="onImageRemove"></a>
- **<u>onImageUpdate</u>** → Método que recibe un índice de la imagen que se quiere actualizar (que en este caso siempre sera el índice 0)., y abre el explorador de archivos para seleccionar una nueva imagen. <a id="onImageUpdate"></a>

```jsx
<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
	{({
		imageList,
		onImageUpload,
		dragProps,
		isDragging,
		onImageRemove,
		onImageUpdate,
	}) => (
		
	)}
</ImageUploading>
```

## 🟣 Creando el componente de Box Drag & Drop. <a id="7"></a>

La función dentro del componente `<ImageUploading/>` debe retornar JSX

Dentro de la carpeta `src/components` creamos un archivo llamado `BoxDragAndDrop.tsx`

Este componente es donde se hará el drag & drop o se dará click para seleccionar alguna imagen

Agregamos el siguiente código: 

```jsx
import React from 'react';

interface Props{
  onImageUpload: () => void;
  dragProps: any;
  isDragging: boolean
}
  
export const BoxDragAndDrop = ({ isDragging, onImageUpload, dragProps }:Props) => {
    return (
      <div
        onClick={onImageUpload}
        {...dragProps}
        className={`container-dnd center-flex ${isDragging ? 'isDragging' : ''}`}
      >
        <span className='label-dnd'>Chosee an Image or Drag and Drop an Image 📤</span>
      </div>
    )
  }
```

Luego agregamos el componente `BoxDragAndDrop.tsx` en el componente `DragAndDrop.tsx`

Dentro de la función haremos una condicional, dependiendo de la lista de imágenes, si esta vacía debe mostrar el componente `BoxDragAndDrop.tsx` sino significa que ya hay una imagen seleccionada y debe mostrar dicha imagen.

```jsx
<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
  {({
    imageList,
    onImageUpload,
    dragProps,
    isDragging,
    onImageRemove,
    onImageUpdate,
  }) => (
    <>
      {
        imageList[0]
          ? <p>SELECTED IMAGE</p>
          : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
      }
    </>
  )}
</ImageUploading>
```

En el componente `BoxDragAndDrop.tsx` se nota tal vez una sintaxis rara, es una forma diferente de pasar propiedades, solo lo hice para ahorrar un par de lineas. Aunque, si es difícil de leer puedes optar por la otra forma. 

```jsx
<BoxDragAndDrop dragProps={dragProps} isDragging={isDragging} onImageUpload={onImageUpload}/>
```

![Box component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w1u0oh6kfho9k48y8n12.png)

## 🟣 Creando el componente de Image Selected. <a id="8"></a>

Dentro de la carpeta `src/components` creamos un archivo llamado `ImageSelected.tsx`

Este componente mostrara la imagen que se ha seleccionado, así como 3 botones los cuales servirán para:

- Subir la imagen a Cloudinary
- Remover la imagen seleccionada
- Actualizar la imagen seleccionada.

Agregamos el siguiente código: 

```jsx
import React from 'react';

interface Props {
  loading: boolean;
  img: string;
  onUpload: () => Promise<void>;
  onImageRemove: (index: number) => void;
  onImageUpdate: (index: number) => void
}

export const ImageSelected = ({ 
	img, 
	loading, 
	onUpload, 
	onImageRemove, 
	onImageUpdate 
}: Props) => {

  return (
    <div>
      <img className='image-selected' src={img} alt='image-selected' width={300} />
      <div className='container-buttons'>
        {
          loading
            ? <p className='loading-label'>Upload image ⏳...</p>
            : <>
              <button disabled={loading} onClick={onUpload}>Upload 📤</button>
              <button disabled={loading} onClick={() => onImageUpdate(0)}>Update ✏️</button>
              <button disabled={loading} onClick={() => onImageRemove(0)}>Cancel ❌</button>
            </>
        }
      </div>
    </div>
  )
}
```

Este componente recibe 5 parámetros: 

- **<u>img</u>** → la imagen seleccionada que se mostrará en pantalla
- **<u>loading</u>** → valor booleano que indicara cuando la imagen este siendo cargada a Cloudinary.
- **<u>onUpload</u>** → Método el cual se encargara de subir la imagen a Cloudinary (se explica más a detalle a continuación)
- [**<u>onImageRemove</u>**](#onImageRemove)
- [**<u>onImageUpdate</u>**](#onImageUpdate)

Luego agregamos el componente `ImageSelected.tsx` en el componente `DragAndDrop.tsx`

Les marcara error, ya que le faltan los parámetros que son obligatorios, por lo que los tenemos que crear.

```jsx
<ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
  {({
    imageList,
    onImageUpload,
    dragProps,
    isDragging,
    onImageRemove,
    onImageUpdate,
  }) => (
    <>
      {
        imageList[0]
          ? <ImageSelected  />
          : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
      }
    </>
  )}
	</ImageUploading>
```

![Image Selected Component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lril1lise4gljyo5d41j.png)

## 🟣 Llenando el componente con funciones y estado. <a id="9"></a>

En el componente `DragAndDrop.tsx` necesitaremos agregar un nuevo estado para manejar el loading y otro estado para agregar la URL que la imagen ya guardada en cloudinary.

Agregamos la función onUpload, que por el momento no hace nada, aún.

```jsx
export const DragAndDrop = () => {
  const [images, setImages] = useState<ImageListType>([]);
	const [urlImage, setUrlImage] = useState('')
  const [loading, setLoading] = useState(false);

  const handleChange = (imageList: ImageListType) => setImages(imageList);
	
	const onUpload = () => {}

  return (
    <>
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ?  <ImageSelected  />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )
}
```

Después ya podemos pasarle los parámetros al componente `<ImageSelected/>`

El parámetro img se obtiene de la propiedad imageList en la posición 0 accediendo a la propiedad dataURL.

```jsx
<ImageSelected img={imageList[0].dataURL!}  {...{ onImageRemove, onUpload, onImageUpdate, loading }} />
```

### 🟠 Agregando la funcionalidad para subir imágenes a Cloudinary. <a id="10"></a>

Antes de ir al método **onUpload**, debemos prepara la función para hacer la llamada a la API de cloudinary. Para ello creamos la carpeta `src/utils` y dentro creamos el archivo `fileUpload.ts` y agregamos lo siguiente:

Creamos la función asíncrona fileUpload que recibe una imagen de tipo File y retorna un string que sera la URL de la imagen o null.

Aquí haremos uso de los datos que configuramos en cloudinary anteriormente. (el nombre de la nube y el preestablecido).

Sera mejor colocar dichos valores en variables de entorno, ya que son delicadas.

```jsx
/*
const cloud_name = process.env.REACT_APP_CLOUD_NAME;
const preset = process.env.REACT_APP_PRESET;
*/
const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {};
```

Luego construimos la URL para hacer la llamada a la API.

```jsx
const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);

    try {
        const res = await fetch(cloudinaryUrl, {
            method: 'POST',
            body: formData
        });

        if (!res.ok) return null;
        
        const data = await res.json();
        return data.secure_url;

    } catch (error) {
        return null;
    }
};
```

Luego construimos la data que vamos enviar a la API, en este caso la imagen.

```jsx
const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);
};
```

Finalmente hacemos uso de la API fetch para hacer la petición y mandar la data.

Si la respuesta no es correcta retornamos null y si no retornamos la URL de la imagen.

```jsx
const cloud_name = 'example-cloud-name';
const preset = 'example-preset';

export const fileUpload = async (file: File): Promise<string | null> => {
    const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${cloud_name}/image/upload`

    const formData = new FormData();
    formData.append('upload_preset', `${preset}`)
    formData.append('file', file);

    try {
        const res = await fetch(cloudinaryUrl, {
            method: 'POST',
            body: formData
        });

        if (!res.ok) return null;
        
        const data = await res.json();
        return data.secure_url;

    } catch (error) {
        return null;
    }
};
```

Ahora sí, es hora de usar la función que acabamos de crear.

1. Primero colocamos el loading en true.
2. Hacemos la llamada a la función fileUpload y le mandamos el valor del estado (recordando que es un arreglo de ImageListType, así que accedemos a la posición 0 a la propiedad file).
3. Luego colocamos el loading en false.
4. Evaluamos si la URL no es null.
    1. Si No es nula, actualizamos el estado y guardamos esa URL.
    2. Si es nula, mandamos una alerta de Error.
5. Finalmente, vaciamos el estado de las imagen seleccionada.

```jsx
const onUpload = async () => {
  setLoading(true);
  const url = await fileUpload(images[0].file!);
  setLoading(false);

  if (url) setUrlImage(url);
  else alert('Error, please try again later. ❌')

  setImages([]);
}
```

## 🟣 Mostrar link de la imagen subida a Cloudinary. <a id="11"></a>

Dentro de la carpeta `src/components` creamos un archivo llamado `Message.tsx`

El cual recibe la URL de la imagen, que puede ser null o un string.

```jsx
import React from 'react';

interface Props {
    urlImage: string | null
}

export const Message = ({ urlImage }: Props) => {
    return (
        <>
            {
                urlImage && <span className='url-cloudinary-sumbit'>
                    Your Image uploaded successfully! ✅ 
										<a target='_blank' href={urlImage}> View Image</a>
                </span>
            }
        </>
    )
}
```

Luego agregamos el componente `Message.tsx` en el componente `DragAndDrop.tsx` y le pasamos el valor del estado de urlImage.

```jsx
return (
    <>
      <Message urlImage={urlImage} />
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ? <ImageSelected  {...{ onImageRemove, onImageUpdate, onUpload, loading }} img={imageList[0].dataURL!} />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )
```

![Message Component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7g43ssd6e0cp5tdnjqft.png)

## 🟣 Ocultar link de la imagen después de unos segundos. <a id="12"></a>

El en componente `DragAndDrop.tsx` agregaremos un efecto. Lo que hará es que, después de 5 segundos, pondrá el valor del estado de urlImage en string vacío, lo que hará que no se cree debido a la condicional.

```jsx
useEffect(() => {
  let timeout: NodeJS.Timeout;

  if(urlImage){
    timeout = setTimeout(()=> {
      setUrlImage('')
    }, 5000)
  }

  return () => {
   clearTimeout(timeout);
  }
}, [urlImage])
```

## 🟣 Refactorizando el componente Drag & Drop y creando un custom hook. <a id="13"></a>

Hay demasiada lógica en el componente, la cual podemos colocar en un custom hook.

Para ello creamos la carpeta Dentro de la carpeta `src/hooks`

Dentro de esa carpeta creamos el archivo `useUploadImage.ts` y movemos la lógica dentro de este hook.

```jsx
import {useEffect, useState} from 'react';
import { ImageListType } from "react-images-uploading";
import { fileUpload } from "../utils";

export const useUploadImage = () => {

    const [images, setImages] = useState<ImageListType>([]);
    const [loading, setLoading] = useState(false);
    const [urlImage, setUrlImage] = useState('')
  
    const handleChange = (imageList: ImageListType) => setImages(imageList);
  
    const onUpload = async () => {
      setLoading(true);
      const url = await fileUpload(images[0].file!);
      setLoading(false);

      if (url) setUrlImage(url);
      else alert('Error, please try again later. ❌')

      setImages([]);
    }
  
    useEffect(() => {
      let timeout: NodeJS.Timeout;
      if(urlImage){
        timeout = setTimeout(()=> {
          setUrlImage('')
        }, 5000)
      }
    
      return () => {
       clearTimeout(timeout);
      }
    }, [urlImage])

    return {
        loading,
        onUpload,
        handleChange,
        urlImage,
        images
    }
}
```

Y de esta manera nos quedaría el componente `DragAndDrop.tsx`

Nota que al componente ImageSelected le quitamos las propiedades loading y onUpload. y le pasamos `…rest`.

```jsx
import React from 'react';
import ImageUploading from "react-images-uploading";
import { useUploadImage } from '../hooks';

import { ImageSelected, BoxDragAndDrop, Message } from './';

export const DragAndDrop = () => {

  const { urlImage, handleChange, images, ...rest } = useUploadImage();

  return (
    <>
      <Message urlImage={urlImage} />
      <ImageUploading multiple={false} value={images} onChange={handleChange} maxNumber={1}>
        {({
          imageList,
          onImageUpload,
          dragProps,
          isDragging,
          onImageRemove,
          onImageUpdate,
        }) => (
          <>
            {
              imageList[0]
                ? <ImageSelected  {...{ onImageRemove, onImageUpdate, ...rest }} img={imageList[0].dataURL!} />
                : <BoxDragAndDrop {...{ onImageUpload, dragProps, isDragging }} />
            }
          </>
        )}
      </ImageUploading>
    </>
  )
}
```
---
**Gracias por llegar hasta aquí!👐👐**
Te dejo el código por si lo quieres revisar! ⬇️
{% embed https://github.com/Franklin361/upload-image-app %}