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
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",
});
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;
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;
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;
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;
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;
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;
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>
);
}
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>
);
}
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>
);
}
You should get a result similar to this:
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)
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:
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...).
Great article, I would recommend using debounce instead of setTimeout 🙌
Came here to say the exact same thing.
Thanks Joel! 👊
Thank you! I have to try it! 🤓
I just love the banner photo btw 👌🏼
I love it too 😭
For some reason this helped me wrap my head around custom hooks. Thanks 💯
I am immensely happy to know! ☺️
Hey, it doesn't work on Chrome mobile browser
Does not work? And on the desktop? 🧐
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/
Hadn't tested it. Thank you very much for your comment. 👊
Thank you too! Your explanations have made it easier to wrap my head around React hooks.
I'm so glad to know. 😊
Thanks for your comment and tip! 👊
Woooow and Wot ! Noh that’s was very amazing I LOVE It
Thanks for the feedback! 🥺
awesome
Thanks! 🥳
I think debounce can also resolve this issue.
I didn't actually try it, but it makes sense 🤔
Very well explained!!!
I'm glad! Thank you very much! 🤩