frontend-challenges
frontend-challenges copied to clipboard
380 - News Board - react
App.jsx
import { useState, useEffect } from "react";
const PAGE_SIZE = 5;
export default function App() {
const [page, setPage] = useState(1);
const [storyIDs, setStoryIDs] = useState([]);
const [stories, setStories] = useState([]);
const [error, setError] = useState(null);
const [isLoadingIDs, setIsLoadingIDs] = useState(false);
const [isLoadingStories, setIsLoadingStories] = useState(false);
useEffect(() => {
const controller = new AbortController();
setIsLoadingIDs(true);
fetchWithRetry("https://hacker-news.firebaseio.com/v0/topstories.json", {
signal: controller.signal,
})
.then(setStoryIDs)
.catch((err) => {
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
setError(err instanceof Error ? err.message : "Unknown error");
})
.finally(() => {
setIsLoadingIDs(false);
});
return () => {
controller.abort("Abort Error");
};
}, []);
// fetch story based on the current page
useEffect(() => {
if (storyIDs.length === 0) return;
const controller = new AbortController();
setIsLoadingStories(true);
const startIndex = (page - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
const promises = [];
for (let i = startIndex; i < endIndex; i++) {
if (!storyIDs[i]) return;
promises.push(
fetchWithRetry(
`https://hacker-news.firebaseio.com/v0/item/${storyIDs[i]}.json`,
{
signal: controller.signal,
},
),
);
}
Promise.all(promises)
.then((result) => {
setStories((stories) => [...stories, ...result]);
})
.catch((err) => {
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
setError(err instanceof Error ? err.message : "Unknown error");
})
.finally(() => {
setIsLoadingStories(false);
});
return () => {
controller.abort("Abort Error");
};
}, [page, storyIDs]);
return (
<>
<h1>News Board</h1>
{stories.length === 0 && (isLoadingIDs || isLoadingStories) && (
<div>Loading...</div>
)}
<ul aria-busy={isLoadingIDs || isLoadingStories}>
{!error &&
stories.map((story) => {
return (
<li key={story.id}>
<a href={story.url} target="_blank" rel="noopener noreferrer">
{story.title}
</a>
</li>
);
})}
</ul>
{stories.length > 0 && !error && (
<button
onClick={() => setPage((prev) => prev + 1)}
disabled={
!isLoadingStories &&
(page - 1) * PAGE_SIZE + PAGE_SIZE >= storyIDs.length
}
>
{isLoadingStories ? "Loading..." : "Load More"}
</button>
)}
{error && <div>{error}</div>}
</>
);
}
const fetchWithRetry = async (url, retries = 2) => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Network error");
return res.json();
} catch (err) {
if (retries > 0) return fetchWithRetry(url, retries - 1);
throw err;
}
};
index.jsx
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<App />
);