Build a job board that displays the latest job postings fetched from the Hacker News API, with each posting displaying the job title, poster, and date posted.
Requirements
- The page should show 6 jobs on initial load with a button to load more postings.
- Clicking the "Load more" button will load the next page of 6 postings. The button does not appear if there aren't any more postings to load.
- If there's a URL field returned for the job details, make the job title a link that opens the job details page in a new window when clicked.
- The timestamp can be formatted in any way you like.
API
Hacker News has a public API to fetch jobs by Y Combinator companies. There's no single API that fetches a list of jobs together with the data, so you will have to make separate requests to fetch the necessary data and combine them to be displayed.
Job Stories
Fetches a list of job posting IDs.
- URL:
https://hacker-news.firebaseio.com/v0/jobstories.json - HTTP Method: GET
- Content Type: json
Sample response:
[35908337, 35904973, 35900922, 35893439, 35890114, 35880345, ...]Job Details
Fetches job posting details given its ID.
- URL:
https://hacker-news.firebaseio.com/v0/item/{id}.json - HTTP Method: GET
- Content Type: json
Sample response for https://hacker-news.firebaseio.com/v0/item/35908337.json:
{
"by": "jamilbk",
"id": 35908337,
"score": 1,
"time": 1683838872,
"title": "Firezone (YC W22) is hiring Elixir and Rust engineers",
"type": "job",
"url": "https://www.ycombinator.com/companies/firezone/jobs"
}Notes
- The focus of this question is on functionality and not on styling, but feel free to beautify the page.
- To improve the user experience and avoid overfetching, you may want to limit the number of job details fetched to the number of jobs visible on the page.
- A job that was deleted on Hacker News comes back as
null. The code keeps it injobs, sojobs.lengthstill works as the offset for the next page, and filters it out when rendering.
Analysis of Hacker News Jobs Board Implementation
Table of Contents
- JavaScript Methods Used
- React Hooks Used
- Spread Operator Explanation
- Complete Code Flow
- Multiple useEffect Explanation
- Complete Code Breakdown
JavaScript Methods Used
1. Array.map()
Purpose: Transform each element in an array
Example:
const jobPromises = idArr.map((id) =>
axios.get(`${JOB_DETAILS_API}${id}.json`).then((res) => res.data)
);Why: Converts array of IDs to array of API request promises
2. Array.slice()
Purpose: Get subset of an array
Example:
const idArr = ids.slice(start, end);Why: Gets only the IDs needed for current page
3. Promise.all()
Purpose: Handle multiple promises concurrently
Example:
const newJobs = await Promise.all(jobPromises);Why: Fetches all job details in parallel for efficiency
4. Date.toLocaleString()
Purpose: Format dates
Example:
date.toLocaleString("en-US", { ...options });Why: Converts Unix timestamp to readable date format
5. try/catch/finally
Purpose: Error handling
Example:
try {
// API calls
} catch (err) {
// Handle error
} finally {
// Cleanup
}Why: Gracefully handles network errors
React Hooks Used
1. useState
Purpose: Manage component state
Examples:
const [ids, setIds] = useState([]); // Job IDs
const [jobs, setJobs] = useState([]); // Job details
const [loading, setLoading] = useState(false); // Loading state
const [error, setError] = useState(null); // Error state2. useEffect
Purpose: Side effects management
Examples:
// Initial data fetch
useEffect(() => {
getJobIds();
}, []);
// Fetch first page when IDs load
useEffect(() => {
if (ids.length > 0 && jobs.length === 0) {
getJobDetails(0, PER_PAGE);
}
}, [ids]);Spread Operator Explanation
setJobs(prevJobs => [...prevJobs, ...newJobs])
Why this pattern:
- Immutability: React state should never be mutated directly
- Functional Update: Ensures we have latest state value
- Array Concatenation: Combines existing jobs with new jobs
Breakdown:
prevJobs: Current state value...prevJobs: Spreads existing jobs into new array...newJobs: Spreads newly fetched jobs into same array- Creates new array combining both
Alternative (problematic):
setJobs([...jobs, ...newJobs]);
// Might use stale 'jobs' value if multiple updates occur quicklyComplete Code Flow
-
Component Mounts
- Initializes all state variables
- First
useEffecttriggersgetJobIds()
-
Fetch Job IDs
- Makes API call to
jobstories.json - On success: Updates
idsstate - On failure: Sets error state
- Makes API call to
-
IDs Loaded
- Second
useEffectdetectsidschange - If no jobs loaded yet, fetches first page (6 jobs)
- Second
-
Fetch Job Details
- Takes slice of IDs (first 6 initially)
- Creates parallel requests for each job
- Updates
jobsstate when all complete - Handles errors appropriately
-
User Interaction
- "Load more" button appears when more jobs available
- Clicking it fetches next page of jobs
- Button disabled during loading
-
Render Cycle
- Maps through
jobsto display cards - Formats dates properly
- Shows loading/error states
- Maps through
Multiple useEffect Explanation
Why Two useEffect Hooks?
- First useEffect:
useEffect(() => {
getJobIds();
}, []);- Purpose: Initial data fetch (job IDs)
- Dependencies: Empty array = runs once on mount
- Why Separate: Job IDs only need to be fetched once
- Second useEffect:
useEffect(() => {
if (ids.length > 0 && jobs.length === 0) {
getJobDetails(0, PER_PAGE);
}
}, [ids]);- Purpose: Fetch first page of jobs when IDs are available
- Dependencies: Runs when
idschanges - Why Separate: Need to wait for IDs before fetching details
- Condition: Only runs if no jobs loaded yet (
jobs.length === 0)
Benefits of This Structure:
- Separation of Concerns: Different triggers for different actions
- Proper Sequencing: IDs must be fetched before details
- Performance: Avoids unnecessary fetches
- Cleaner Code: Each effect has single responsibility
Complete Code Breakdown
1. Configuration Constants
const JOB_API = "https://hacker-news.firebaseio.com/v0/jobstories.json";
const JOB_DETAILS_API = "https://hacker-news.firebaseio.com/v0/item/";
const PER_PAGE = 6;- Centralizes API endpoints
- Makes page size configurable
2. State Management
const [ids, setIds] = useState([]); // All job IDs
const [jobs, setJobs] = useState([]); // Jobs to display
const [loading, setLoading] = useState(false); // Loading state
const [error, setError] = useState(null); // Error handling- Comprehensive state for all UI needs
3. Data Fetching Functions
getJobIds:
- Fetches all job IDs from API
- Simple error handling
- Sets loading state appropriately
getJobDetails:
- Takes start/end indexes for pagination
- Uses
sliceto get current page IDs - Parallel requests with
Promise.all - Immutable state update
- Comprehensive error handling
4. Helper Functions
loadMoreJobs:
- Calculates next page indexes
- Calls
getJobDetailswith proper range
formatDate:
- Converts Unix timestamp
- Locale-aware formatting
- Consistent date display
5. Render Logic
- Conditional rendering of error messages
- Job cards with proper linking
- Loading state indicators
- "Load more" button with disabled state
- Clean separation of concerns in JSX
Key Architecture Decisions
-
Pagination:
- Client-side pagination for better UX
- Loads only needed data
- Smooth "load more" experience
-
Error Handling:
- User-friendly error messages
- Console logging for debugging
- Doesn't break UI on errors
-
Loading States:
- Prevents duplicate requests
- Gives user feedback
- Disables buttons during load
-
Data Flow:
- Clear sequence: IDs → Details
- Proper state dependencies
- No race conditions