Image search engine with React JS - React Query 🔋
Tags: react, javascript, tutorial, beginners
Published: 2022-10-07 12:28:13

This time we will make an image search engine with the help of [Unsplash API](https://unsplash.com/) 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.🤗

&nbsp;

## Table of Contents.
> 📌 [Technologies to be used.](#1)  
> 📌 [Creating the project.](#2)  
> 📌 [First steps.](#3)  
> 📌 [Creating the form.](#4)  
>> 📌 [Handling the form submit event.](#5)  
>
> 📌 [Creating the cards and doing the image search.](#6)  
>> 📌 [Making the request to the API.](#7)  
>
> 📌 [Conclusion.](#8)  
>> 📌 [Demo of the application.](#9)  
>> 📌 [Source code.](#10)  


&nbsp;


## 💧 Technologies to be used. <a id="1"></a>

- ▶️ React JS (v 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ React Query
- ▶️ Axios
- ▶️ [Unsplash API](https://unsplash.com/)
- ▶️ CSS vanilla (You can find the styles in the repository at the end of this post) 


## 💧 Creating the project. <a id="2"></a>

We will name the project: **`search-images`** (optional, you can name it whatever you like).
```bash
npm init vite@latest
```
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.

```bash
cd search-images
```
Then we install the dependencies.
```bash
npm install
```
Then we open the project in a code editor (in my case VS code).
```bash
code .
```

## 💧 First steps. <a id="3"></a>
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`.
```tsx
const App = () => {
    return (
        <div>Hello world</div>
    )
}
export default App
```

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

```tsx
export const Title = () => {
    return (
        <>
            <h1>Search Image</h1>
            <hr />
        </>
    )
}
```
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.

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

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. 

```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;
}
```

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.

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

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6btiy0lf5t8g21do1k8.png)


## 💧 Creating the form. <a id="4"></a>

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

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

Now let's place it in **src/App.tsx**.
```tsx
import { Title } from './components/Title';
import { Form } from './components/Form';

const App = () => {

  return (
    <div>
      <Title />
      <Form/>
    </div>
  )
}
export default App
```
And it should look something like this 👀.

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

### 💧 Handling the form submit event. <a id="5"></a>

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

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

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.

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

First, we prevent the default behavior


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

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.

```ts
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() 
    const target = e.target as HTMLFormElement;
}
```
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 


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

    const target = e.target as HTMLFormElement;

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

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.

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

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.

```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()
}
```

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

```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()
}
```

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:

```tsx

export const useFormQuery = () => {

}
```

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**.
```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
    }
}
```

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**.

```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
```


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


## 💧 Creating the cards and doing the image search. <a id="6"></a>

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.

```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**

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.

```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>
        </>
    )
}
```

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.

```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
```

### 💧 Making the request to the API. <a id="7"></a>

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](https://tanstack.com/query/v4/?from=reactQueryV3&original=https://react-query-v3.tanstack.com/)

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
```

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**.

```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>
)
```

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.

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

- 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**, 

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

We build the URL, it is worth mentioning that we need an API Key to use this API. Just create an [Unsplash account](https://unsplash.com/join). Create an app and get the access key.

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

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.
```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)
    }
}
```

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.

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

✅ Do it like this.
```ts
useQuery([query], () => getImages(query))
```

And to have typing we are going to put that the data is of type ResponseAPI.
```ts
useQuery<ResponseAPI>([query], () => getImages(query))
```

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.

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

Then it would look like this.

```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>
        </>
    )
}
```

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.

```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>
        </>
    )
}
```

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

Showing the loading:

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

Showing search results:


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


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.

```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>
    )
}
```
In the hook **useFormQuery.ts** we create a new state that will start with the value false.
```ts
const [isLoading, setIsLoading] = useState(false)
```
And a function to update this status:
```ts
const handleLoading = (loading: boolean) => setIsLoading(loading)
```

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

```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
    }
}
```

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.

```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
```

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.


```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>
        </>
    )
}
```


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

## 💧 Conclusion. <a id="8"></a>
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](https://franklin-dev.netlify.app/)**

> 🔵 Don't forget to follow me also on twitter: [**@Frankomtz361**](https://twitter.com/Frankomtz361)

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

### 💧 Demo of the application. <a id="9"></a>
[https://search-image-unsplash.netlify.app](https://search-image-unsplash.netlify.app/)

### 💧 Source code. <a id="10"></a>

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