Skip to the solution
MediumFrontend11 min read

Build a Job Board (paginated fetch)

441 views

The problem

Question presented to candidate: "Build a job board on top of the Hacker News API. One endpoint gives you the ids of the latest job posts; another gives you the details for a single id. Show the first six jobs with their title (linking to the posting when there is a URL), the poster and the date, and a Load more button that fetches the next six. You will not have network access in this exercise, so simulate the two endpoints in the file and say so."

Requirements:

  • Simulate jobstories.json (an array of ids, newest first) and item/<id>.json (one job: id, by, time in unix seconds, title, optional url) with setTimeout delays, including different delays per item so responses arrive out of order.
  • On load: fetch the ids once, then the details of the first six in parallel.
  • "Load more jobs" fetches the next six; while loading it reads "Loading…" and is disabled, and a second click does nothing; it disappears when everything is loaded.
  • Each card: the title as a link (target="_blank" with rel="noopener noreferrer") when there is a URL, plain text otherwise, plus "By author · date" using a <time> element.
  • A failed request shows an alert with a Retry button that retries exactly what failed.
  • Cards stay in id order no matter which request answers first.

What a strong answer should cover:

  • The two-step data flow: ids first (cheap, one request), then details per page with Promise.all, and why Promise.all keeps results in request order.
  • Loading, idle and error as explicit states, and guarding against double clicks and duplicate pages.
  • Cleanup so a request that finishes after unmount (or after the React StrictMode double mount in development) does not set state.
  • Converting unix seconds to milliseconds and formatting dates with Intl.DateTimeFormat.
  • Trade-offs: Promise.all fails the whole page if one item fails (versus Promise.allSettled), and when infinite scroll would replace the button.

Clarifying questions expected:

  • "Should a single failed job hide the whole page, or should the rest still show?"
  • "Is a Load more button fine, or do you want infinite scroll?"
  • "How should a job without a URL behave?"

Code / implementation expected: Yes. A working page in React, Vue or Angular over an in-file simulated API, with loading, error and retry states.

  • Data fetching
  • Pagination
  • Async

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read · 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useEffect, useRef, useState } from "react";
import { fetchJob, fetchJobIds } from "./src/api/hnApi";
import JobCard from "./src/components/JobCard";

const PAGE_SIZE = 6;

export default function App() {
  const [ids, setIds] = useState(null); // every job id, from the first request
  const [jobs, setJobs] = useState([]); // details loaded so far, in id order
  // "loading" | "idle" | "error"
  const [status, setStatus] = useState("loading");
  const [failNext, setFailNext] = useState(false);
  const inFlight = useRef(false);

  // Fetch the details for the next PAGE_SIZE ids, all at once.
  async function loadPage(allIds, from) {
    if (inFlight.current) return; // a second click while loading does nothing
    inFlight.current = true;
    setStatus("loading");
    const fail = failNext;
    setFailNext(false);
    try {
      const next = await Promise.all(
        allIds
          .slice(from, from + PAGE_SIZE)
          .map((id) => fetchJob(id, { fail })),
      );
      setJobs((prev) => [...prev, ...next]);
      setStatus("idle");
    } catch {
      setStatus("error");
    } finally {
      inFlight.current = false;
    }
  }

  function loadIds() {
    let ignore = false;
    setStatus("loading");
    fetchJobIds()
      .then((all) => {
        if (ignore) return;
        setIds(all);
        loadPage(all, 0);
      })
      .catch(() => !ignore && setStatus("error"));
    return () => {
      ignore = true;
    };
  }

  // StrictMode mounts twice in development; the
  // cleanup makes the first run ignore its answer.
  useEffect(loadIds, []);

  const hasMore = ids !== null && jobs.length < ids.length;
  const retry = () => (ids ? loadPage(ids, jobs.length) : loadIds());

  return (
    <main className="board">
      <h1>Hacker News Jobs</h1>
      <p className="note">
        Data comes from an in-file simulation of the Hacker News API.
      </p>

      {ids === null && status === "loading" && (
        <p data-testid="status" role="status">
          Loading jobs…
        </p>
      )}

      <ul className="jobs" aria-busy={status === "loading"}>
        {jobs.map((job) => (
          <JobCard key={job.id} job={job} />
        ))}
      </ul>

      {ids !== null && (
        <p data-testid="progress" className="note" aria-live="polite">
          Showing {jobs.length} of {ids.length} jobs
        </p>
      )}

      {status === "error" && (
        <p role="alert" data-testid="error" className="error">
          Could not load jobs.{" "}
          <button type="button" data-testid="retry" onClick={retry}>
            Retry
          </button>
        </p>
      )}

      {hasMore && status !== "error" && (
        <button
          type="button"
          data-testid="more"
          className="more"
          disabled={status === "loading"}
          onClick={() => loadPage(ids, jobs.length)}
        >
          {status === "loading" ? "Loading…" : "Load more jobs"}
        </button>
      )}

      <label className="note sim">
        <input
          type="checkbox"
          data-testid="fail-next"
          checked={failNext}
          onChange={(e) => setFailNext(e.target.checked)}
        />
        Simulate a network error on the next load
      </label>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 56 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

Build a Custom Multi-select Dropdown

Continue