Buscador de imágenes con React JS - React Query 🔋
Tags: javascript, react, tutorial, spanish
Published: 2022-10-07 12:28:51

Esta vez haremos un buscador de imágenes con ayuda de la [API de Unsplash](https://unsplash.com/) y **React Query**, con el cual notaras un gran cambio en tus aplicaciones, con tan pocas lineas de código, React Query mejorara el rendimiento de tu aplicación! 

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

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

&nbsp;

## Tabla de contenido
> 📌 [Tecnologías a utilizar.](#1)  
> 📌 [Creando el proyecto.](#2)  
> 📌 [Primeros pasos.](#3)  
> 📌 [Creando el formulario.](#4)  
>> 📌 [Manejando el evento submit del formulario.](#5)  
>
> 📌 [Creando las tarjetas y haciendo la búsqueda de imágenes.](#6)  
>> 📌 [Haciendo la petición a la API.](#7)  
>
> 📌 [Conclusión.](#8)  
>> 📌 [Demo de la aplicación.](#9)  
>> 📌 [Código fuente.](#10)  

&nbsp;

## 💧 Tecnologías a utilizar. <a id="1"></a>

- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ React Query
- ▶️ Axios
- ▶️ [Unsplash API](https://unsplash.com/)
- ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post) 


## 💧 Creando el proyecto. <a id="2"></a>

Al proyecto le colocaremos el nombre de: **`search-images`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
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.

```bash
cd search-images
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

## 💧 Primeros pasos. <a id="3"></a>
Creamos las siguientes carpetas: 

- **src/components**
- **src/interfaces**
- **src/hooks**
- **src/utils**

Dentro del archivo **src/App.tsx** borramos todo y creamos un componente que muestre un `hola mundo`
```tsx
const App = () => {
    return (
        <div>Hello world</div>
    )
}
export default App
```

Luego creamos dentro de la carpeta **src/components** el archivo **Title.tsx** y agregamos el siguiente código, el cual solo muestra un simple titulo.

```tsx
export const Title = () => {
    return (
        <>
            <h1>Search Image</h1>
            <hr />
        </>
    )
}
```
Dentro de esa misma carpeta vamos a crear un archivo **Loading.tsx** y agregamos lo siguiente que actuara como loading cuando la información se cargue.

```ts
export const Loading = () => {
    return (
        <div className="loading">
            <div className="spinner"></div>
            <span>Loading...</span>
        </div>
    )
}
```

De una vez vamos a establecer la interfaz de la respuesta de la API, dentro de la carpeta de **src/interfaces** creamos un archivo `index.ts` y agregamos la siguientes interfaces. 

```ts
export interface ResponseAPI {
    results: Result[];
}

export interface Result {
    id: string;
    description: null | string;
    alt_description: null | string;
    urls: Urls;
    likes: number;
}

export interface Urls {
    small: string;
}
```

La API devuelve mas información pero solo necesito esa por el momento.  

Una vez que ya tenemos el titulo, vamos a colocarlo en el archivo **src/App.tsx**

```tsx
import { Title } from './components/Title';

const App = () => {
  return ( <div> <Title /> </div> )
}
export default App
```

y se vería algo asi 👀 (los estilos los puedes revisar en el código en Github, el enlace esta al final de este articulo).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6btiy0lf5t8g21do1k8.png)

## 💧 Creando el formulario. <a id="4"></a>

Dentro de la carpeta **src/components** creamos el archivo `Form.tsx` y agregamos el siguiente formulario.

```tsx
export const Form = () => {
    return (
        <form>
            <input type="text" placeholder="Example: superman" />
            <button>Search</button>
        </form>
    )
}
```

Ahora vamos a colocarlo en el **src/App.tsx**
```tsx
import { Title } from './components/Title';
import { Form } from './components/Form';

const App = () => {

  return (
    <div>
      <Title />
      <Form/>
    </div>
  )
}
export default App
```
Y debería lucir algo asi 👀

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nv7ugv6864je25pbp9c.png)

### 💧 Manejando el evento submit del formulario. <a id="5"></a>

Vamos a pasarla al evento onSubmit del form una función nombrada **handleSubmit**

```tsx
export const Form = () => {
    return (
        <form onSubmit={handleSubmit}>
            <input type="text" placeholder="Example: superman" />
            <button>Search</button>
        </form>
    )
}
```

Esta función hará lo siguiente.  
Recibirá el evento, en el cual tendremos todo lo necesario para recuperar todos los datos de cada input dentro del formulario que en este caso es solo un input.

```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    
}
```

Primero, prevenimos el comportamiento por defecto


```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() 
}
```

Luego creamos una variable (target) y vamos a castear el la propiedad target del evento, para que nos ayude al autocompletado.

```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() 
    const target = e.target as HTMLFormElement;
}
```
Ahora vamos a usar la función fromEntries de la instancia de Object mandando una nueva instancia de FormData el caul a su vez recibe la propiedad target del evento. 
Esto nos devolverá cada uno de los valores dentro de nuestro formulario. Y el cual podemos desestructurar.
Aunque no nos ayuda el autocompletado para desestructurar cada valor del input 


```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const target = e.target as HTMLFormElement;

    const { form } = Object.fromEntries(new FormData(target))
}
```

Por cierto, nota que desestructuro una propiedad llamada **form** y de donde saco eso?

Pues eso dependerá del **valor** que le hayas dado a tu propiedad **name** en el input.

```html
    <input type="text" placeholder="Example: superman" name="form" />
```

Bueno, ya obtuvimos el valor del input, ahora vamos a validar que si su longitud es 0, que no haga nada.
Y si esa condición no se cumple, entonces ya tendremos nuestra palabra clave para buscar imágenes.

Por cierto, también borrar el formulario y ponerle el foco en el input.

```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const target = e.target as HTMLFormElement;

    const { form } = Object.fromEntries(new FormData(target))

    if (form.toString().trim().length === 0) return

    target.reset()
    target.focus()
}
```

Ahora para lo que si usaremos un estado, es para mantener ese valor del input.
Creamos un estado. Y le enviamos el valor de nuestro input

```tsx
const [query, setQuery] = useState('')

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const target = e.target as HTMLFormElement;

    const { form } = Object.fromEntries(new FormData(target))

    if (form.toString().trim().length === 0) return

    setQuery(form.toString())

    target.reset()
    target.focus()
}
```

Todo bien, pero ahora el problema es que lo tenemos todo en el componente **Form.tsx** y necesitamos compartir el estado **query** para comunicar que imagen vamos a buscar.

Asi que lo mejor es que movamos este código, primero a un custom hook.

Dentro de la carpeta **src/hook** creamos un archivo **index.tsx** y agregamos la siguiente función:

```tsx

export const useFormQuery = () => {

}
```

Movemos la función **handleSubmit** dentro del hook y también el estado.
Y retornamos el valor del estado (**query**) y la función **handleSubmit**
```tsx
import { useState } from 'react';

export const useFormQuery = () => {

    const [query, setQuery] = useState('')

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()

        const target = e.target as HTMLFormElement;

        const { form } = Object.fromEntries(new FormData(target))

        if (form.toString().trim().length === 0) return

        setQuery(form.toString())

        target.reset()
        target.focus()
    }

    return {
        query, handleSubmit
    }
}
```

Luego llamemos al hook en el componente padre del **Form.tsx** que es el **src/App.tsx** y le pasemos al Form.tsx la función **handleSubmit**

```tsx
import { Title } from './components/Title';
import { Form } from './components/Form';
import { useFormQuery } from "./hooks";

const App = () => {

  const { handleSubmit, query } = useFormQuery()

  return (
    <div>
      <Title />

      <Form handleSubmit={handleSubmit} />

    </div>
  )
}
export default App
```


y al componente **Form.tsx** le agregamos la siguiente interface.

```tsx
interface IForm {
    handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
}

export const Form = ({ handleSubmit }: IForm) => {
    return (
        <form onSubmit={handleSubmit}>
            <input type="text" name="form" placeholder="Example: superman" />
            <button>Search</button>
        </form>
    )
}
```

## 💧 Creando las tarjetas y haciendo la búsqueda de imágenes. <a id="6"></a>

Vamos a la carpeta **src/components** y creamos 2 archivos nuevos.

1 - **Card.tsx**

Aquí solo haremos un componente que recibe como props la información de la imagen. La interfaz Result ya la habíamos definido con anterioridad

```tsx
import { Result } from "../interface"

interface ICard {
    res: Result
}

export const Card = ({ res }: ICard) => {
    return (
        <div>
            <img src={res.urls.small} alt={res.alt_description || 'photo'} loading="lazy" />
            <div className="hidden">
                <h4>{res.description}</h4>
                <b>{res.likes} ❤️</b>
            </div>
        </div>
    )
}
```


2 - **GridResults.tsx**

Por el momento solo vamos a realizar el cascaron de este componente.

Este componente, va a recibir la query (imagen a buscar) por props.

Aquí es donde se hará la petición a la API y mostrar las tarjetas.

```tsx
import { Card } from './Card';

interface IGridResults {
    query: string
}

export const GridResults = ({ query }: IGridResults) => {

    return (
        <>
            <p className='no-results'>
                Results with: <b>{query}</b>
            </p>

            <div className='grid'>
                {/* TODO: map to data and show cards */}
            </div>
        </>
    )
}
```

Ahora vamos a usar nuestro **GridResults.tsx** en **src/App.tsx**

Lo mostraremos de forma condicional, donde si el valor del estado **query** (la imagen a buscar) tiene una longitud mayor a 0, entonces se muestra el componente y muestra los resultados que coincidan con la búsqueda.

```tsx
import { Title } from './components/Title';
import { Form } from './components/Form';
import { GridResults } from './components/GridResults';

import { useFormQuery } from "./hooks";

const App = () => {

  const { handleSubmit, query } = useFormQuery()

  return (
    <div>
      <Title />

      <Form handleSubmit={handleSubmit} />

      {query.length > 0 && <GridResults query={query} />}
    </div>
  )
}
export default App
```

### 💧 Haciendo la petición a la API. <a id="7"></a>

Par hacer la petición, lo haremos de una mejor manera, en vez de hacer un típico fetch con useEffect.

Usaremos axios y [react query](https://tanstack.com/query/v4/?from=reactQueryV3&original=https://react-query-v3.tanstack.com/)

React Query facilita la obtención, el almacenamiento en caché y la gestión de los datos. Y es lo que recomienda el equipo de React en vez de hacer una simple petición fetch dentro de un useEffect.

Ahora vamos a la terminal a instalar estas dependencias:
```
npm install @tanstack/react-query axios
```

Luego de instalar las dependencias, necesitamos envolver  nuestra app con el proveedor de React query.
Para ello, vamos al punto mas alto de nuestra app, que es el archivo **src/main.tsx**

Primero creamos el cliente de React Query.

Envolvemos el componente App con el **QueryClientProvider** y le mandamos en la prop **client** nuestro **queryClient**

```tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import './index.css'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
)
```

Ahora en el componente **GridResults.tsx** ...

Usaremos un hook de react-query que es el **useQuery**, el cual recibe 3 parámetros, pero por el momento solo usaremos los primeros dos.

- El primer parámetro es la queryKey que es un arreglo de valores (arreglos con valores tan simples como un string o complejos como un objeto), nos sirve para identificar los datos que se almacenaban en el cache. En este caso mandamos un arreglo de con el valor de la query.

```ts
useQuery([query])
```

- El segundo parámetro el la queryFn, es la función que hace la petición y retorna una promesa ya resuelta con los datos o un error.
Para esto vamos a crear nuestra función, en la carpeta **src/utils** creamos el archivo **index.ts** y creamos una función.

Esta función es asíncrona y recibe una query de tipo string y retorna una promesa de tipo **ResponseAPI**, 

```ts
export const getImages = async (query: string): Promise<ResponseAPI> => {
    
}
```

Construimos la URL, cabe mencionar que necesitamos una API Key para usar esta API. Solo vas a crear una [cuenta en Unsplash](https://unsplash.com/join). creas una app y obtienes la llave de acceso.

```ts
export const getImages = async (query: string): Promise<ResponseAPI> => {
   const url = `https://api.unsplash.com/search/photos?query=${query}&client_id=${ACCESS_KEY}` 
}
```

Luego hacemos un try/catch por si algo sale mal.
Dentro del **try** hacemos la petición con ayuda de axios. Hacemos un get y mandamos la url, desestructuramos la propiedad data y la retornamos.

En el **catch** solo lanzaremos un error mandando el mensaje.
```ts
import axios from 'axios';
import { ResponseAPI } from "../interface"
import { AxiosError } from 'axios';

const ACCESS_KEY = import.meta.env.VITE_API_KEY as string

export const getImages = async (query: string): Promise<ResponseAPI> => {
    const url = `https://api.unsplash.com/search/photos?query=${query}&client_id=${ACCESS_KEY}`
    try {
        const { data } = await axios.get(url)
        return data
    } catch (error) {
        throw new Error((error as AxiosError).message)
    }
}
```

Ahora si, vamos a usar nuestra función **getImages**, se la mandamos al hook.
Pero, como esta función recibe un parámetro, necesitamos mandarla de la siguiente manera: creamos una nueva función que retorna el getImages y mandamos la query que nos llega por props

❌ No lo hagas asi.

```ts
useQuery([query], getImages(query))
```

✅ Hazlo asi.
```ts
useQuery([query], () => getImages(query))
```

Y para tener tipado vamos a poner que la data es de tipo ResponseAPI.
```ts
useQuery<ResponseAPI>([query], () => getImages(query))
```

Finalmente, desestructuramos lo que necesitamos del hook

- **data**: Los datos que retorna nuestra función **getImages**.
- **isLoading**: valor booleano, que nos indica cuando se esta haciendo una petición.
- **error**: el mensaje de error, si es que hay uno, por defecto es undefined.
- **isError**: valor booleano, que nos indica si hay un error.

```ts
const { data, isLoading, error, isError } = useQuery<ResponseAPI>([query], () => getImages(query))
```

Entonces se vería asi.

```tsx
import { Card } from './Card';

interface IGridResults {
    query: string
}

export const GridResults = ({ query }: IGridResults) => {

    const { data, isLoading, error, isError } = useQuery<ResponseAPI>(['images', query], () => getImages(query))

    return (
        <>
            <p className='no-results'>
                Results with: <b>{query}</b>
            </p>

            <div className='grid'>
                {/* TODO: map to data and show cards */}
            </div>
        </>
    )
}
```

Ahora que ya tenemos la data, vamos a mostrar unas cuantos componentes aquí.

1 - Primero una condición, para saber si isLoading esta en verdadero, mostramos el componente **Loading.tsx**.

2 - Segundo, al finalizar el loading, evaluamos si hay un error, y si existe mostramos el error.

3 - Luego hacemos una condición dentro del elemento p donde si no hay resultados de la búsqueda, mostramos un texto u otro.

4 - Finalmente, recorremos la data para mostrar las imágenes.

```tsx
import { useQuery } from '@tanstack/react-query';

import { Card } from './Card';
import { Loading } from './Loading';

import { getImages } from "../utils"
import { ResponseAPI } from '../interface';

interface IGridResults {
    query: string
}

export const GridResults = ({ query }: IGridResults) => {

    const { data, isLoading, error, isError } = useQuery<ResponseAPI>([query], () => getImages(query))

    if (isLoading) return <Loading />

    if (isError) return <p>{(error as AxiosError).message}</p>

    return (
        <>
            <p className='no-results'>
                {data && data.results.length === 0 ? 'No results with: ' : 'Results with: '}
                <b>{query}</b>
            </p>

            <div className='grid'>
                {data.results.map(res => (<Card key={res.id} res={res} />))}
            </div>
        </>
    )
}
```

Y Listo, ya podríamos dejarlo asi y se vería muy bien

Mostrando el loading:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v2odohrc3m8co6t75itm.png)

Mostrando los resultados de la búsqueda:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3r4d8x4qy7ve8o61i0sq.png)

Pero me gustaría bloquear el formulario mientras el loading esta activo.

Para esto el componente **Form.tsx** debe recibir otra prop que es **isLoading** y colocarlo en los valores de las propiedades de **disable** tanto del input y del botón

```tsx
interface IForm {
    handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
    isLoading: boolean
}

export const Form = ({ handleSubmit, isLoading }: IForm) => {
    return (
        <form onSubmit={handleSubmit}>
            <input type="text" name="form" disabled={isLoading} placeholder="Example: superman" />
            <button disabled={isLoading}>Search</button>
        </form>
    )
}
```
En el hook **useFormQuery.ts** creamos un estado nuevo que se iniciara con el valor falso.
```ts
const [isLoading, setIsLoading] = useState(false)
```
Y una función para actualizar dicho estado:
```ts
const handleLoading = (loading: boolean) => setIsLoading(loading)
```

Y los retornamos el valor de **isLoading** y la función **handleLoading**.

```tsx
import { useState } from 'react';

export const useFormQuery = () => {

    const [query, setQuery] = useState('')

    const [isLoading, setIsLoading] = useState(false)

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()

        const target = e.target as HTMLFormElement;

        const { form } = Object.fromEntries(new FormData(target))

        if (form.toString().trim().length === 0) return

        setQuery(form.toString())

        target.reset()
        target.focus()
    }

    const handleLoading = (loading: boolean) => setIsLoading(loading)

    return {
        query, isLoading, handleSubmit, handleLoading
    }
}
```

En **src/App.tsx** desestructuramos isLoading y handleSubmit del hook. Y isLoading se lo mandamos al componente **Form** y la función se la mandamos al **GridResults**

```tsx
import { Title } from './components/Title';
import { Form } from './components/Form';
import { GridResults } from './components/GridResults';
import { useFormQuery } from "./hooks";

const App = () => {

  const { handleLoading, handleSubmit, isLoading, query } = useFormQuery()

  return (
    <div>
      <Title />

      <Form handleSubmit={handleSubmit} isLoading={isLoading} />

      {query.length > 0 && <GridResults query={query} handleLoading={handleLoading} />}
    </div>
  )
}
export default App
```

En el componente **GridResults.tsx** vamos a recibir la nueva prop que es **handleLoading**, lo desestructuramos, y dentro del componente realizamos un useEffect antes de las condiciones, y dentro del useEffect ejecutamos **handleLoading** y le mandamos el valor de **isLoading** que nos da el hook **useQuery** y el useEffect se ejecutara cada vez qie el valor **isLoading** cambie, por eso lo colocamos como dependencia del useEffect.


```tsx
import { useEffect } from 'react';
import { AxiosError } from 'axios';
import { useQuery } from '@tanstack/react-query';

import { Card } from './Card';
import { Loading } from './Loading';

import { getImages } from "../utils"
import { ResponseAPI } from '../interface';

interface IGridResults {
    handleLoading: (e: boolean) => void
    query: string
}

export const GridResults = ({ query, handleLoading }: IGridResults) => {

    const { data, isLoading, error, isError } = useQuery<ResponseAPI>([query], () => getImages(query))

    useEffect(() => handleLoading(isLoading), [isLoading])

    if (isLoading) return <Loading />

    if (isError) return <p>{(error as AxiosError).message}</p>


    return (
        <>
            <p className='no-results'>
                {data && data.results.length === 0 ? 'No results with: ' : 'Results with: '}
                <b>{query}</b>
            </p>

            <div className='grid'>
                {data.results.map(res => (<Card key={res.id} res={res} />))}
            </div>
        </>
    )
}
```


Y Listo, asi bloquearemos el formulario cuando se este ejecutando la petición.

## 💧 Conclusión. <a id="8"></a>
Espero que te haya gustado esta publicación y que te haya ayudada a entender un nuevo enfoque para realizar peticiones con **react-query**! y crecer tu interés en esta librería que es muy usada y muy util, con el cual notas cambios increíbles en el rendimiento de tu app. 🤗

Si conoces alguna otra forma distinta o mejor de realizar esta aplicación con gusto puedes comentarla

> **Te invito a que revises mi portafolio en caso de que estés interesado en contactarme para algún proyecto! [Franklin Martinez Lucas](https://franklin-dev.netlify.app/)**

> 🔵 No olvides seguirme también en twitter: [**@Frankomtz361**](https://twitter.com/Frankomtz361)

{%embed https://twitter.com/Frankomtz361/status/1575329958674796547?s=20&t=AVAVQ1am_AkFb2TyMo9S-A %}


### 💧 Demo de la aplicación. <a id="9"></a>
[https://search-image-unsplash.netlify.app](https://search-image-unsplash.netlify.app/)

### 💧 Código fuente. <a id="10"></a>

{%embed https://github.com/Franklin361/search-images %}