DEV Community

Cover image for How to Create a Search Bar in React
Francisco Mendes
Francisco Mendes

Posted on

How to Create a Search Bar in React

One of the things I was currently interested in was creating a search bar, however I didn't want to search for things that were already available in the frontend. In the same way that I didn't feel like making a request to an Api by clicking on a button.

I just wanted to use an Input and as soon as I finish writing, I would automatically make the request to the Api and this is where the challenge of today's example lies.

The idea of today's example is to write the name of a house from Game of Thrones and then we will list the family name and its members.

Let's code

In today's example we'll just install axios to make http requests for our application:

npm i axios
Enter fullscreen mode Exit fullscreen mode

The application scaffold is up to you because it will work anyway, whether you use create-react-app, vite or other.

First we will create the axios instance and then consume the Game Of Thrones Quotes API.

// @src/api/got.js

import axios from "axios";

export default axios.create({
  baseURL: "https://game-of-thrones-quotes.herokuapp.com/v1/house",
});
Enter fullscreen mode Exit fullscreen mode

Then we can start working on our custom hook. For this to work we are going to use two well known hooks that you are probably familiar with, useState() and useEffect(). Just like we're going to import our axios instance.

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  // ...
  return;
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Let's start by creating our state, which in this example will be an object with two properties. We have the slug, which will be the search term, this being the name of the house. And the second will be the results of Api's response.

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  const [data, setData] = useState({
    slug: "",
    results: [],
  });
  // ...
  return;
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Now we can use useEffect(), which will be executed whenever the slug is changed. This way:

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  const [data, setData] = useState({
    slug: "",
    results: [],
  });

  useEffect(() => {
    // ...
  }, [data.slug]);

  return;
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

However, as you might have already thought. If we now make the http request inside useEffect(), it means that whenever the user writes a new character, he will make a new request to the Api.

But we don't want that, so we'll use setTimeout(), because we want the http request to be done as soon as the user finishes writing, for that we'll have a timeout of one second.

But then we will be taking a risk, we could end up having several timeouts and we don't want that. This is because if the timeout starts and the user writes again, we want to cancel the previous timeout. So we need to clean up our useEffect() and we will use clearTimeout() to cancel the previous timeout. Like this:

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  const [data, setData] = useState({
    slug: "",
    results: [],
  });

  useEffect(() => {
    if (data.slug !== "") {
      const timeoutId = setTimeout(() => {
        // ...
      }, 1000);
      return () => clearTimeout(timeoutId);
    }
  }, [data.slug]);

  return;
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Now we can make our http request using our axios instance and let's pass our slug as the only parameter. We will then store the response data in our state.

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  const [data, setData] = useState({
    slug: "",
    results: [],
  });

  useEffect(() => {
    if (data.slug !== "") {
      const timeoutId = setTimeout(() => {
        const fetch = async () => {
          try {
            const res = await got.get(`/${data.slug}`);
            setData({ ...data, results: res.data });
          } catch (err) {
            console.error(err);
          }
        };
        fetch();
      }, 1000);
      return () => clearTimeout(timeoutId);
    }
  }, [data.slug]);

  return;
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Now just return our state and our setter to be able to use our custom hook.

// @src/hooks/useFetch.js

import { useState, useEffect } from "react";

import got from "../api/got";

const useFetch = () => {
  const [data, setData] = useState({
    slug: "",
    results: [],
  });

  useEffect(() => {
    if (data.slug !== "") {
      const timeoutId = setTimeout(() => {
        const fetch = async () => {
          try {
            const res = await got.get(`/${data.slug}`);
            setData({ ...data, results: res.data });
          } catch (err) {
            console.error(err);
          }
        };
        fetch();
      }, 1000);
      return () => clearTimeout(timeoutId);
    }
  }, [data.slug]);

  return { data, setData };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Now we can start by creating our UI, first let's start with our App.jsx. Likewise, we'll import our custom hook and import the House.jsx component (which hasn't been created yet) and we'll do conditional rendering, as we'll only show house data if we have some.

// @src/App.jsx

import React from "react";

import useFetch from "./hooks/useFetch";
import House from "./components/House";

export default function App() {
  const { data, setData } = useFetch();
  return (
    <main>
      <input
        type="text"
        placeholder="Type your favorite house"
        value={data.slug}
        onChange={(e) => setData({ ...data, slug: e.target.value })}
      />
      <br />
      {data.results.length > 0 ? <House family={data.results[0]} /> : null}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now we can start creating our House.jsx component, and in the same we will import Members.jsx (which has yet to be created).

// @src/components/House.jsx

import React from "react";

import Members from "./Members";

export default function House({ family }) {
  return (
    <div>
      <h1>{family.name}</h1>
      <Members members={family.members} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally we can create our last component that will list each of the family members.

// @src/components/Members.jsx

import React from "react";

export default function Members({ members }) {
  return (
    <ul>
      {members.map((el, i) => (
        <li key={i}>{el.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

You should get a result similar to this:

final app

Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🪗

Hope you have a great day! 👋 😜

Top comments (26)

Collapse
 
logharaa profile image
Gaël • Edited

Thank you for this interesting article!

I found a way to do the same thing but without useEffect() and in a more optimized way because there is no re-rendering needed each time a key is pressed:

export function SearchBar() {
    let timeoutID = -1;

    function handleChange(e) {
        const searchText = e.target.value;
        clearTimeout(timeoutID);
        timeoutID = setTimeout(() => {
            // Do the fetch and update state here.
        }, 700);
    }

    return <input [...] onChange={handleChange} />;
}
Enter fullscreen mode Exit fullscreen mode

This approach also follows the official website's advice to only use the useEffect hook if there is no other way (react.dev/learn/you-might-not-need...).

Collapse
 
emi_castellano profile image
Emi Castellano

Great article, I would recommend using debounce instead of setTimeout 🙌

Collapse
 
joelazevedo profile image
Joel Azevedo

Came here to say the exact same thing.

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks Joel! 👊

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thank you! I have to try it! 🤓

Collapse
 
georgy5 profile image
Karl Keller

I just love the banner photo btw 👌🏼

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

I love it too 😭

Collapse
 
kellykels21 profile image
Marke'l Spellman • Edited

For some reason this helped me wrap my head around custom hooks. Thanks 💯

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

I am immensely happy to know! ☺️

Collapse
 
anvilicious profile image
Nigel Lowa

Hey, it doesn't work on Chrome mobile browser

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Does not work? And on the desktop? 🧐

Collapse
 
anvilicious profile image
Nigel Lowa

It works on the desktop just fine. Sorry, I was in a hurry so couldn't explain myself better earlier. The API only accepts lowercase slugs so I had to e.target.value.toLowerCase() all slug values for the API to respond to my get requests when I use it on mobile. I was following your tutorial -- here's my netflify build angry-bhaskara-6c8f3a.netlify.app/

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes • Edited

Hadn't tested it. Thank you very much for your comment. 👊

Thread Thread
 
anvilicious profile image
Nigel Lowa

Thank you too! Your explanations have made it easier to wrap my head around React hooks.

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes

I'm so glad to know. 😊

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for your comment and tip! 👊

Collapse
 
sorindezzy profile image
Raita Dezideriu

Woooow and Wot ! Noh that’s was very amazing I LOVE It

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for the feedback! 🥺

Collapse
 
luiznasciment0 profile image
Luiz Nascimento

awesome

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks! 🥳

Collapse
 
vucat12 profile image
vucat12

I think debounce can also resolve this issue.

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

I didn't actually try it, but it makes sense 🤔

Collapse
 
codbugs profile image
Coding Bugs

Very well explained!!!

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

I'm glad! Thank you very much! 🤩