DEV Community

Cover image for Image search engine with React JS - React Query ๐Ÿ”‹
Franklin Martinez
Franklin Martinez

Posted on

Image search engine with React JS - React Query ๐Ÿ”‹

This time we will make an image search engine with the help of Unsplash API and React Query, with which you will notice a big change in your applications, with so few lines of code, React Query will improve the performance of your application!

๐Ÿšจ Note: This post requires you to know the basics of React with TypeScript (basic hooks).

Any kind of feedback is welcome, thanks and I hope you enjoy the article.๐Ÿค—

ย 

Table of Contents.

๐Ÿ“Œ Technologies to be used.

๐Ÿ“Œ Creating the project.

๐Ÿ“Œ First steps.

๐Ÿ“Œ Creating the form.

๐Ÿ“Œ Handling the form submit event.

๐Ÿ“Œ Creating the cards and doing the image search.

๐Ÿ“Œ Making the request to the API.

๐Ÿ“Œ Conclusion.

๐Ÿ“Œ Demo of the application.

๐Ÿ“Œ Source code.

ย 

๐Ÿ’ง Technologies to be used.

  • โ–ถ๏ธ React JS (v 18)
  • โ–ถ๏ธ Vite JS
  • โ–ถ๏ธ TypeScript
  • โ–ถ๏ธ React Query
  • โ–ถ๏ธ Axios
  • โ–ถ๏ธ Unsplash API
  • โ–ถ๏ธ CSS vanilla (You can find the styles in the repository at the end of this post)

๐Ÿ’ง Creating the project.

We will name the project: search-images (optional, you can name it whatever you like).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.

cd search-images
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ง First steps.

We create the following folders:

  • src/components
  • src/interfaces
  • src/hooks
  • src/utils

Inside the src/App.tsx file we delete everything and create a component that displays a hello world.

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

Then we create inside the folder src/components the file Title.tsx and add the following code, which only shows a simple title.

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

Inside that same folder we are going to create a Loading.tsx file and add the following that will act as loading when the information is loaded.

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

At once we are going to set the API response interface, inside the folder src/interfaces we create a file index.ts and add the following 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

The API returns more information but I only need that for the moment.

Once we have the title, let's place it in the src/App.tsx file.

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

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

and it would look something like this ๐Ÿ‘€ (you can check the styles in the code on Github, the link is at the end of this article).

Image description

๐Ÿ’ง Creating the form.

Inside the folder src/components we create the file Form.tsx and add the following form.

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

Now let's place it in 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

And it should look something like this ๐Ÿ‘€.

Image description

๐Ÿ’ง Handling the form submit event.

We are going to pass to the onSubmit event of the form a function named handleSubmit.

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

This function will do the following.

It will receive the event, in which we will have all the necessary to recover all the data of each input inside the form that in this case is only one input.

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

}
Enter fullscreen mode Exit fullscreen mode

First, we prevent the default behavior

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

Then we create a variable (target) and we are going to set the target property of the event, so that it helps us with the autocompletion.

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

Now we are going to use the fromEntries function of the Object instance sending a new instance of FormData which in turn receives the target property of the event.
This will return us each one of the values inside our form. And which we can destructure.
Although it doesn't help us the autocompletion to destructure each value of the 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

By the way, note that I destruct a property called form and where do I get that from?

Well that will depend on the value you have given to your name property in the input.

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

Well, we have already obtained the value of the input, now we are going to validate that if its length is 0, that it does nothing.
And if that condition is not met, then we will have our keyword to search for images.

By the way, also delete the form and put the focus on the 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

Now what we will use a state for, is to maintain that input value.
We create a state. And we send it the value of our 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

All good, but now the problem is that we have it all in the Form.tsx component and we need to share the query state to communicate what image we are going to look for.

So the best thing to do is to move this code, first to a custom hook.

Inside the folder src/hook we create a file index.tsx and add the following function:


export const useFormQuery = () => {

}
Enter fullscreen mode Exit fullscreen mode

We move the handleSubmit function inside the hook and also the state.
And we return the value of the state (query) and the function 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

Then let's call the hook in the parent component of Form.tsx which is src/App.tsx and pass to Form.tsx the function 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

and to the Form.tsx component we add the following 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

๐Ÿ’ง Creating the cards and doing the image search.

Go to the src/components folder and create 2 new files.

1 - Card.tsx

Here we will only make a component that receives as props the information of the image. The interface Result has already been defined before.

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

For the moment we are only going to make the shell of this component.

This component will receive the query (image to search) by props.

This is where the request to the API will be made and display the cards.

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

Now let's use our GridResults.tsx in src/App.tsx.

We will display it conditionally, where if the value of the query state (the image to search for) has a length greater than 0, then the component is displayed and shows the results that match the search.

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

๐Ÿ’ง Making the request to the API.

To make the request, we will do it in a better way, instead of doing a typical fetch with useEffect.

We will use axios and react query

React Query makes it easy to fetch, cache and manage data. And that's what the React team recommends instead of doing a simple fetch request inside a useEffect.

Now let's go to the terminal to install these dependencies:

npm install @tanstack/react-query axios
Enter fullscreen mode Exit fullscreen mode

After installing the dependencies, we need to wrap our app with the React query provider.
To do this, we go to the highest point of our app, which is the src/main.tsx file.

First we create the React Query client.

We wrap the App component with the QueryClientProvider and send it in the client prop our 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

Now in the GridResults.tsx component ...

We will use a react-query hook that is the useQuery, which receives 3 parameters, but for the moment we will only use the first two.

  • The first parameter is the queryKey which is an array of values (arrays with values as simple as a string or complex as an object), it is used to identify the data that was stored in the cache. In this case we send an array of with the value of the query.
useQuery([query])
Enter fullscreen mode Exit fullscreen mode
  • The second parameter is the queryFn, it is the function that makes the request and returns a promise already resolved with the data or an error. For this we are going to create our function, in the src/utils folder we create the index.ts file and create a function.

This function is asynchronous and receives a query of type string and returns a promise of type ResponseAPI,

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

}
Enter fullscreen mode Exit fullscreen mode

We build the URL, it is worth mentioning that we need an API Key to use this API. Just create an Unsplash account. Create an app and get the access key.

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

Then we do a try/catch in case something goes wrong.
Inside the try we make the request with the help of axios. We do a get and send the url, unstructure the data property and return it.

In the catch we will only throw an error sending the message.

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

Now if we are going to use our function getImages, we send it to the hook.
But, as this function receives a parameter, we need to send it in the following way: we create a new function that returns the getImages and we send the query that arrives to us by props

โŒ Don't do it that way.

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

โœ… Do it like this.

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

And to have typing we are going to put that the data is of type ResponseAPI.

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

Finally, we deconstruct what we need from the hook

  • data: The data returned by our getImages function.
  • isLoading: boolean value, that tells us when a request is being made.
  • error: the error message, if there is one, by default is undefined.
  • isError: boolean value, that indicates if there is an error.
const { data, isLoading, error, isError } = useQuery<ResponseAPI>([query], () => getImages(query))
Enter fullscreen mode Exit fullscreen mode

Then it would look like this.

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

Now that we have the data, let's show a few components here.

1 - First a condition, to know if isLoading is true, we show the component Loading.tsx.

2 - Second, at the end of the loading, we evaluate if there is an error, and if there is, we show the error.

3 - Then we make a condition inside the p element where if there are no search results, we display one text or another.

4 - Finally, we go through the data to show the images.

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

And that's it, we could leave it like that and it would look very nice.

Showing the loading:

Image description

Showing search results:

Image description

But I would like to block the form while the loading is active.

For this the Form.tsx component must receive another prop which is isLoading and place it in the disable property values of both the input and the button.

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

In the hook useFormQuery.ts we create a new state that will start with the value false.

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

And a function to update this status:

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

And we return the value of isLoading and the handleLoading function.

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

In src/App.tsx we unstructure isLoading and handleSubmit of the hook. And isLoading we send it to the Form component and the function we send it to the GridResults component.

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

In the component GridResults.tsx we are going to receive the new prop that is handleLoading, we unstructure it, and inside the component we make a useEffect before the conditions, and inside the useEffect we execute handleLoading and we send the value of isLoading that gives us the hook useQuery and the useEffect will be executed every time that the value isLoading changes, for that reason we place it as dependency of the 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

And ready, this way we will block the form when the request is being executed.

๐Ÿ’ง Conclusion.

I hope you liked this post and that it helped you to understand a new approach to make requests with react-query! and grow your interest in this library that is very used and very useful, with which you notice incredible changes in the performance of your app. ๐Ÿค—

If you know of any other different or better way to perform this application, please feel free to comment. ๐Ÿ™Œ.

I invite you to check my portfolio in case you are interested in contacting me for a project!. Franklin Martinez Lucas

๐Ÿ”ต Don't forget to follow me also on twitter: @Frankomtz361

๐Ÿ’ง Demo of the application.

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

๐Ÿ’ง Source code.

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 ๐Ÿ‘€

Top comments (4)

Collapse
 
egolegegit profile image
egolegegit

A simple and useful example!
Very well presented!
Thank you!

Collapse
 
flash010603 profile image
Usuario163

It's been a while since you last posted! ๐Ÿ˜”
Excellent tutorial, well explained and very useful! ๐ŸŽ‰

Collapse
 
vishwastyagi profile image
Vishwas Tyagi

Very useful tutorial.

Collapse
 
wallterr profile image
Walter

Excelente amigo! Thanks for your contribution. It helped me a lot with React.