frontend-challenges icon indicating copy to clipboard operation
frontend-challenges copied to clipboard

380 - News Board - react

Open jsartisan opened this issue 3 months ago • 0 comments

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

jsartisan avatar Aug 18 '25 05:08 jsartisan