Buscador de imágenes con React JS - React Query 🔋

franklin030601

Franklin Martinez

Posted on October 7, 2022

Buscador de imágenes con React JS - React Query 🔋

Esta vez haremos un buscador de imágenes con ayuda de la API de Unsplash 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.🤗

 

Tabla de contenido

📌 Tecnologías a utilizar.

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Creando el formulario.

📌 Manejando el evento submit del formulario.

📌 Creando las tarjetas y haciendo la búsqueda de imágenes.

📌 Haciendo la petición a la API.

📌 Conclusión.

📌 Demo de la aplicación.

📌 Código fuente.

 

💧 Tecnologías a utilizar.

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

💧 Creando el proyecto.

Al proyecto le colocaremos el nombre de: search-images (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 search-images
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

💧 Primeros pasos.

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

const App = () => {
    return (
        <div>Hello world</div>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

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.

export const Title = () => {
    return (
        <>
            <h1>Search Image</h1>
            <hr />
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

export const Loading = () => {
    return (
        <div className="loading">
            <div className="spinner"></div>
            <span>Loading...</span>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

const App = () => {
  return ( <div> <Title /> </div> )
}
export default App
Enter fullscreen mode Exit fullscreen mode

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

💧 Creando el formulario.

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

export const Form = () => {
    return (
        <form>
            <input type="text" placeholder="Example: superman" />
            <button>Search</button>
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a colocarlo en el src/App.tsx

import { Title } from './components/Title';
import { Form } from './components/Form';

const App = () => {

  return (
    <div>
      <Title />
      <Form/>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Y debería lucir algo asi 👀

Image description

💧 Manejando el evento submit del formulario.

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

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

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.

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

}
Enter fullscreen mode Exit fullscreen mode

Primero, prevenimos el comportamiento por defecto

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() 
}
Enter fullscreen mode Exit fullscreen mode

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

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() 
    const target = e.target as HTMLFormElement;
}
Enter fullscreen mode Exit fullscreen mode

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

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

    const target = e.target as HTMLFormElement;

    const { form } = Object.fromEntries(new FormData(target))
}
Enter fullscreen mode Exit fullscreen mode

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.

    <input type="text" placeholder="Example: superman" name="form" />
Enter fullscreen mode Exit fullscreen mode

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.

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()
}
Enter fullscreen mode Exit fullscreen mode

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

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()
}
Enter fullscreen mode Exit fullscreen mode

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:


export const useFormQuery = () => {

}
Enter fullscreen mode Exit fullscreen mode

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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

💧 Creando las tarjetas y haciendo la búsqueda de imágenes.

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

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

💧 Haciendo la petición a la API.

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

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
Enter fullscreen mode Exit fullscreen mode

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

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>
)
Enter fullscreen mode Exit fullscreen mode

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.
useQuery([query])
Enter fullscreen mode Exit fullscreen mode
  • 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,

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

}
Enter fullscreen mode Exit fullscreen mode

Construimos la URL, cabe mencionar que necesitamos una API Key para usar esta API. Solo vas a crear una cuenta en Unsplash. creas una app y obtienes la llave de acceso.

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

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.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

useQuery([query], getImages(query))
Enter fullscreen mode Exit fullscreen mode

✅ Hazlo asi.

useQuery([query], () => getImages(query))
Enter fullscreen mode Exit fullscreen mode

Y para tener tipado vamos a poner que la data es de tipo ResponseAPI.

useQuery<ResponseAPI>([query], () => getImages(query))
Enter fullscreen mode Exit fullscreen mode

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.
const { data, isLoading, error, isError } = useQuery<ResponseAPI>([query], () => getImages(query))
Enter fullscreen mode Exit fullscreen mode

Entonces se vería asi.

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

Mostrando el loading:

Image description

Mostrando los resultados de la búsqueda:

Image description

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

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

En el hook useFormQuery.ts creamos un estado nuevo que se iniciara con el valor falso.

const [isLoading, setIsLoading] = useState(false)
Enter fullscreen mode Exit fullscreen mode

Y una función para actualizar dicho estado:

const handleLoading = (loading: boolean) => setIsLoading(loading)
Enter fullscreen mode Exit fullscreen mode

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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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.

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

💧 Conclusión.

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

🔵 No olvides seguirme también en twitter: @Frankomtz361

💧 Demo de la aplicación.

https://search-image-unsplash.netlify.app

💧 Código fuente.

GitHub logo Franklin361 / search-images

App to search images with Unsplash's API and react-query 🔋

Creating an infinite scroll with React JS! 🔋

This time, we are going to implement the search images using React JS and React Query!

 

Demo

 

Features ⚙️

  1. Search images by query.
  2. View images.

 

Technologies 🧪

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ React Query
  • ▶️ Unsplash API
  • ▶️ CSS vanilla

 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/search-images
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 👀






💖 💪 🙅 🚩
franklin030601
Franklin Martinez

Posted on October 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related