Public on: 20 Aug, 2023 Written by: Franklin Martinez Lucas
Using relative dates

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

Technologies to be used.

Creating the project.

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

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.

cd search-images

Then we install the dependencies.

npm install

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

code .

First steps.

We create the following folders:

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

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

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

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

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

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

title

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

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

And it should look something like this 👀.

form

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

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>) => {
 
}

First, we prevent the default behavior

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.

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

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.

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

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

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:

 
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 .

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 .

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.

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

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

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

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

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

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

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.

useQuery([query])

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> => {
 
}

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

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

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

✅ Do it like this.

useQuery([query], () => getImages(query))

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

useQuery<ResponseAPI>([query], () => getImages(query))

Finally, we deconstruct what we need from the hook

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

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

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

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

Showing the loading:

loading

Showing search results:

loading

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

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

const [isLoading, setIsLoading] = useState(false)

And a function to update this status:

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

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

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

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

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.


🔵 Don't forget to follow me also on twitter: @Frankomtz030601