data-fetching-with-useeffect
Data fetching with useEffect
import { useEffect, useRef, useState } from "react";
const BASE_URL = "https://jsonplaceholder.typicode.com";
interface Post {
id: number;
title: string;
}
export default function Demo() {
const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(false);
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(0);
const abortControllerRef = useRef<AbortController | null>(null);
useEffect(() => {
const fetchPosts = async () => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setIsLoading(true);
try {
const response = await fetch(`${BASE_URL}/posts?page=${page}`, {
signal: abortControllerRef.current?.signal,
});
const posts = (await response.json()) as Post[];
setPosts(posts);
} catch (e: any) {
if (e.name === "AbortError") {
console.log("Aborted");
return;
}
setError(e);
} finally {
setIsLoading(false);
}
};
fetchPosts();
}, [page]);
if (error) {
return <div>Something went wrong! Please try again.</div>;
}
return (
<div className="tutorial">
<h1 className="mb-4 text-2xl">Data Fething in React</h1>
<button onClick={() => setPage(page + 1)}>Increase Page ({page})</button>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<ul>
{posts.map((post) => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
)}
</div>
);
}
Using fetch()
Fetching a request
const [jobIds, setJobIds] = useState([]);
useEffect(() => {
fetchJobIds();
}, []);
async function fetchJobIds() {
response = await fetch(
`https://hacker-news.firebaseio.com/v0/jobstories.json`
);
data = await response.json(); // don't forget await here
setJobIds(data);
}
The above is the same as below but using arrow function's chaining .then().
const [jobIds, setJobIds] = useState([]);
useEffect(() => {
fetchJobIds();
}, []);
async function fetchJobIds() {
data = await fetch(
`https://hacker-news.firebaseio.com/v0/jobstories.json`
).then((res) => res.json());
setJobIds(data);
}
Fetching multiple requests
const [jobDetails, setJobDetails] = useState([])
const URL = "https://hacker-news.firebaseio.com";
useEffect(() => {
if (!loading) {
fetchJobDetails(page);
}
}, [page])
async function fetchJobIds(currPage) {...}
async function fetchJobDetails(currPage) {
setLoading(true);
const jobIdsForPage = await fetchJobIds(currPage);
const JobDetailsPromise = jobIdsForPage.map((id) => (
fetch(`${URL}/v0/item/${id}.json`).then((res) => res.json())
));
const data = await Promise.all(JobDetailsPromise);
setJobDetails([...jobDetails, ...data]);
setLoading(false);
}
The above is the same as below but using arrow function's chaining .then().
const [jobDetails, setJobDetails] = useState([])
const URL = "https://hacker-news.firebaseio.com";
useEffect(() => {
if (!loading) {
fetchJobDetails(page);
}
}, [page])
async function fetchJobIds(currPage) {...}
async function fetchJobDetails(currPage) {
setLoading(true);
const jobIdsForPage = await fetchJobIds(currPage);
const data = await Promise.all(jobIdsForPage.map((id) =>
fetch(`${URL}/v0/item/${id}.json`).then((res) => res.json())
));
setJobDetails([...jobDetails, ...data]);
setLoading(false);
}
Prevent useEffect() from fetching multiple times
Option 1: Track a loading state to ensures that a new fetch operation doesn't start until the current one is finished. This is simpler and good enough POC for interview setting.
jsx
const [jobDetails, setJobDetails] = useState([])
const [loading, setLoading] = useState(false); // loading state
const URL = "https://hacker-news.firebaseio.com";
useEffect(() => {
if (!loading) {
fetchJobDetails(page);
}
}, [page])
async function fetchJobIds(currPage) {...}
async function fetchJobDetails(currPage) {
setLoading(true);
const jobIdsForPage = await fetchJobIds(currPage);
const data = await Promise.all(jobIdsForPage.map((id) =>
// notice that the fetch is not in a bracket
fetch(`${URL}/v0/item/${id}.json`).then((res) => res.json())
));
setJobDetails([...jobDetails, ...data]);
setLoading(false);
}
Option 2: Using AbortController and loading state. This is handle edge case of when use abort a fetch request while the fetch is still ongoing. This approach ensures that any ongoing fetch operations are properly cancelled when they are no longer needed, preventing multiple fetches from happening simultaneously.
jsx
const [page, setPage] = useState(0);
const [jobDetails, setJobDetails] = useState([]);
const [loading, setLoading] = useState(false); // loading fetch
const [jobIds, setJobIds] = useState([]);
const PAGE_SIZE = 6;
const URL = "https://hacker-news.firebaseio.com";
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
if (!loading) {
fetchJobDetails(page, signal);
}
// useEffect clean-up
return () => {
controller.abort();
};
}, [page]);
// notice that the signal controller was passed to the sub-fetch request as well
async function fetchJobIds(currPage, signal) {
const response = await fetch(`${URL}/v0/jobstories.json`, { signal });
const data = await response.json();
const start = currPage * PAGE_SIZE;
const end = start + PAGE_SIZE;
return data.slice(start, end);
}
async function fetchJobDetails(currPage, signal) {
setLoading(true);
try {
const jobIdsForPage = await fetchJobIds(currPage, signal);
const data = await Promise.all(
jobIdsForPage.map((id) =>
fetch(`${URL}/v0/item/${id}.json`, { signal }).then((res) => res.json())
)
);
setJobDetails([...jobDetails, ...data]);
} catch (error) {
if (error.name !== "AbortError") {
console.log("Fetch Error: ", error);
}
} finally {
setLoading(false);
}
}