Job Board Component with Hacker News API
Master machine coding by building a job board component that fetches and displays job postings from the Hacker News API with pagination, loading states, and responsive design.
Machine Coding Challenge: Job Board Component
Create a production-ready job board component that fetches job postings from the Hacker News API, displays them with pagination, and provides a smooth user experience with loading states and error handling.
Understanding the Problem
Before we dive into coding, let's understand what we're building and why it matters. A job board is a fundamental web application that users rely on daily to discover career opportunities. This challenge tests your ability to work with external APIs, handle asynchronous data, implement pagination, and create a responsive user interface.
Why Job Boards Matter
User Experience Impact:
- Information Discovery: Users can browse multiple job opportunities efficiently
- Time Efficiency: Pagination allows users to load content as needed
- Accessibility: Clear job information helps users make informed decisions
- Mobile Responsiveness: Job seekers use various devices to browse opportunities
Real-World Applications:
- Company career pages and job portals
- Recruitment agency websites
- Freelance platform job listings
- Internal HR systems for job postings
- Startup job boards and tech community sites
What Makes a Great Job Board?
- Fast Loading: Users expect quick access to job information
- Clear Information: Job title, company, and posting date should be prominent
- Responsive Design: Works seamlessly on desktop, tablet, and mobile
- Error Handling: Graceful handling of API failures and network issues
- Accessibility: Screen readers can navigate and understand job listings
- Performance: Efficient data fetching and rendering
🧠 Planning Your Approach
Step 1: Define Your Requirements
Before writing any code, ask yourself these questions:
Functional Requirements:
- How many jobs should be displayed initially? (6 as specified)
- Should jobs be sorted by date, relevance, or other criteria?
- Do you need search or filtering capabilities?
- Should there be loading states while fetching data?
- How should errors be displayed to users?
Technical Constraints:
- What's your time limit? (2-3 hours is typical)
- Which framework are you using? (React, Vue, vanilla JS?)
- What browsers do you need to support?
- Are there any performance requirements?
API Integration Strategy:
- Handle the two-step API process (job IDs → job details)
- Implement proper error handling for network failures
- Consider rate limiting and caching strategies
- Plan for API response format changes
Success Criteria:
- Smooth pagination with "Load More" functionality
- Proper loading states and error handling
- Responsive design across all devices
- Clean, maintainable code structure
Step 2: Choose Your Architecture
Think about your component structure before coding:
Component Hierarchy:
JobBoard (Main Container)
├── JobList (Job Collection)
│ ├── JobCard (Individual Job)
│ └── JobCard (Individual Job)
├── LoadMoreButton (Pagination)
├── LoadingSpinner (Loading State)
└── ErrorMessage (Error State)State Management Strategy:
- Track current page and loaded jobs
- Manage loading and error states
- Cache job data to avoid refetching
- Handle API response data
Data Flow:
User Action → API Call → Data Processing → State Update → UI Update🛠️ Implementation Strategy
Phase 1: Start Simple
Begin with the basics - don't overcomplicate things initially.
-
Create Basic Structure
- Set up your main component structure
- Add basic styling and layout
- Implement static job display
-
Add API Integration
- Fetch job IDs from the API
- Fetch individual job details
- Handle basic error cases
-
Implement Pagination
- Add "Load More" functionality
- Track loaded jobs and current page
- Hide button when no more jobs
Phase 2: Add Advanced Features
Once the basics work, enhance with advanced capabilities.
-
Loading States
- Show loading spinners during API calls
- Disable buttons during loading
- Provide user feedback
-
Error Handling
- Handle network failures gracefully
- Show retry options
- Provide meaningful error messages
-
Performance Optimizations
- Implement proper caching
- Optimize re-renders
- Add virtual scrolling for large lists
Phase 3: Polish and Refine
Focus on the details that make it production-ready.
-
User Experience
- Smooth animations and transitions
- Responsive design for all screen sizes
- Accessibility improvements
-
Code Quality
- Clean component structure
- Proper TypeScript types
- Comprehensive error handling
Design Considerations
Visual Design Principles
Information Hierarchy:
- Job Title: Most prominent (largest font, bold)
- Company/Author: Secondary importance
- Date: Tertiary information (smaller, muted color)
- Actions: Clear call-to-action buttons
Layout Guidelines:
- Card-based Design: Each job in its own container
- Consistent Spacing: Uniform margins and padding
- Visual Separation: Clear boundaries between jobs
- Responsive Grid: Adapts to different screen sizes
Color and Typography:
- High Contrast: Ensure readability
- Consistent Font Sizes: Establish clear hierarchy
- Muted Colors: For secondary information
- Accent Colors: For interactive elements
Accessibility Best Practices
Screen Reader Support:
- Use semantic HTML elements (
<article>,<section>) - Provide descriptive alt text for images
- Use proper heading hierarchy
- Include ARIA labels for interactive elements
Keyboard Navigation:
- Ensure all interactive elements are keyboard accessible
- Provide visible focus indicators
- Support tab navigation through job cards
Color and Contrast:
- Maintain sufficient color contrast (4.5:1 minimum)
- Don't rely solely on color to convey information
- Provide alternative visual indicators
Core Implementation
Basic Job Board Component
Here's the essential structure for your job board component:
// components/JobBoard.tsx
import React, { useState, useEffect, useCallback } from 'react';
import JobCard from './JobCard';
import LoadMoreButton from './LoadMoreButton';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';
interface Job {
id: number;
title: string;
by: string;
time: number;
url?: string;
}
interface JobBoardProps {
jobsPerPage?: number;
className?: string;
}
const JobBoard: React.FC<JobBoardProps> = ({
jobsPerPage = 6,
className = ''
}) => {
const [jobs, setJobs] = useState<Job[]>([]);
const [jobIds, setJobIds] = useState<number[]>([]);
const [currentPage, setCurrentPage] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true);
// Fetch job IDs from Hacker News API
const fetchJobIds = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://hacker-news.firebaseio.com/v0/jobstories.json');
if (!response.ok) {
throw new Error('Failed to fetch job IDs');
}
const ids: number[] = await response.json();
setJobIds(ids);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch jobs');
} finally {
setLoading(false);
}
}, []);
// Fetch individual job details
const fetchJobDetails = useCallback(async (jobId: number): Promise<Job | null> => {
try {
const response = await fetch(`https://hacker-news.firebaseio.com/v0/item/${jobId}.json`);
if (!response.ok) {
throw new Error(`Failed to fetch job ${jobId}`);
}
const jobData = await response.json();
// Validate job data
if (!jobData || jobData.type !== 'job') {
return null;
}
return {
id: jobData.id,
title: jobData.title,
by: jobData.by,
time: jobData.time,
url: jobData.url
};
} catch (err) {
console.error(`Error fetching job ${jobId}:`, err);
return null;
}
}, []);
// Load more jobs
const loadMoreJobs = useCallback(async () => {
if (loading || !hasMore) return;
try {
setLoading(true);
setError(null);
const startIndex = currentPage * jobsPerPage;
const endIndex = startIndex + jobsPerPage;
const jobsToFetch = jobIds.slice(startIndex, endIndex);
if (jobsToFetch.length === 0) {
setHasMore(false);
return;
}
// Fetch job details in parallel
const jobPromises = jobsToFetch.map(fetchJobDetails);
const jobResults = await Promise.all(jobPromises);
// Filter out null results and add to existing jobs
const validJobs = jobResults.filter((job): job is Job => job !== null);
setJobs(prevJobs => [...prevJobs, ...validJobs]);
setCurrentPage(prev => prev + 1);
setHasMore(endIndex < jobIds.length);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load more jobs');
} finally {
setLoading(false);
}
}, [loading, hasMore, currentPage, jobsPerPage, jobIds, fetchJobDetails]);
// Initial load
useEffect(() => {
fetchJobIds();
}, [fetchJobIds]);
// Load initial jobs after job IDs are fetched
useEffect(() => {
if (jobIds.length > 0 && jobs.length === 0) {
loadMoreJobs();
}
}, [jobIds, jobs.length, loadMoreJobs]);
const formatDate = (timestamp: number): string => {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
if (error && jobs.length === 0) {
return (
<div className={`job-board ${className}`}>
<h1 className="job-board__title">Hacker News Jobs Board</h1>
<ErrorMessage
message={error}
onRetry={fetchJobIds}
/>
</div>
);
}
return (
<div className={`job-board ${className}`}>
<h1 className="job-board__title">Hacker News Jobs Board</h1>
<div className="job-board__content">
{jobs.length === 0 && loading ? (
<LoadingSpinner />
) : (
<>
<div className="job-list">
{jobs.map((job) => (
<JobCard
key={job.id}
job={job}
formatDate={formatDate}
/>
))}
</div>
{hasMore && (
<LoadMoreButton
onClick={loadMoreJobs}
loading={loading}
/>
)}
</>
)}
</div>
</div>
);
};
export default JobBoard;Job Card Component
// components/JobCard.tsx
import React from 'react';
interface Job {
id: number;
title: string;
by: string;
time: number;
url?: string;
}
interface JobCardProps {
job: Job;
formatDate: (timestamp: number) => string;
}
const JobCard: React.FC<JobCardProps> = ({ job, formatDate }) => {
const handleJobClick = () => {
if (job.url) {
window.open(job.url, '_blank', 'noopener,noreferrer');
}
};
const JobTitle = job.url ? 'a' : 'h2';
const jobTitleProps = job.url ? {
href: job.url,
target: '_blank',
rel: 'noopener noreferrer',
onClick: handleJobClick,
className: 'job-card__title job-card__title--link'
} : {
className: 'job-card__title'
};
return (
<article className="job-card" role="article">
<JobTitle {...jobTitleProps}>
{job.title}
</JobTitle>
<div className="job-card__metadata">
<span className="job-card__author">By {job.by}</span>
<span className="job-card__date">{formatDate(job.time)}</span>
</div>
{job.url && (
<div className="job-card__actions">
<button
className="job-card__apply-button"
onClick={handleJobClick}
aria-label={`Apply for ${job.title}`}
>
View Job
</button>
</div>
)}
</article>
);
};
export default JobCard;Load More Button Component
// components/LoadMoreButton.tsx
import React from 'react';
interface LoadMoreButtonProps {
onClick: () => void;
loading?: boolean;
disabled?: boolean;
}
const LoadMoreButton: React.FC<LoadMoreButtonProps> = ({
onClick,
loading = false,
disabled = false
}) => {
return (
<div className="load-more-container">
<button
className={`load-more-button ${loading ? 'loading' : ''}`}
onClick={onClick}
disabled={loading || disabled}
aria-label={loading ? 'Loading more jobs...' : 'Load more jobs'}
>
{loading ? (
<>
<span className="load-more-button__spinner"></span>
Loading...
</>
) : (
'Load more'
)}
</button>
</div>
);
};
export default LoadMoreButton;Loading Spinner Component
// components/LoadingSpinner.tsx
import React from 'react';
interface LoadingSpinnerProps {
size?: 'small' | 'medium' | 'large';
message?: string;
}
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
size = 'medium',
message = 'Loading jobs...'
}) => {
return (
<div className={`loading-spinner loading-spinner--${size}`}>
<div className="loading-spinner__spinner"></div>
<p className="loading-spinner__message">{message}</p>
</div>
);
};
export default LoadingSpinner;Error Message Component
// components/ErrorMessage.tsx
import React from 'react';
interface ErrorMessageProps {
message: string;
onRetry?: () => void;
}
const ErrorMessage: React.FC<ErrorMessageProps> = ({ message, onRetry }) => {
return (
<div className="error-message" role="alert">
<div className="error-message__icon">⚠️</div>
<h2 className="error-message__title">Something went wrong</h2>
<p className="error-message__text">{message}</p>
{onRetry && (
<button
className="error-message__retry-button"
onClick={onRetry}
>
Try Again
</button>
)}
</div>
);
};
export default ErrorMessage;Essential CSS Styling
/* components/JobBoard.css */
.job-board {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.job-board__title {
font-size: 2.5rem;
font-weight: 700;
color: #1a1a1a;
text-align: center;
margin-bottom: 2rem;
}
.job-board__content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.job-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.job-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.job-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.job-card__title {
font-size: 1.25rem;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 0.75rem 0;
line-height: 1.4;
}
.job-card__title--link {
color: #2563eb;
text-decoration: none;
cursor: pointer;
}
.job-card__title--link:hover {
color: #1d4ed8;
text-decoration: underline;
}
.job-card__metadata {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 1rem;
}
.job-card__author {
font-weight: 500;
}
.job-card__date {
color: #9ca3af;
}
.job-card__actions {
margin-top: 1rem;
}
.job-card__apply-button {
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.job-card__apply-button:hover {
background: #1d4ed8;
}
.load-more-container {
display: flex;
justify-content: center;
margin-top: 2rem;
}
.load-more-button {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.load-more-button:hover:not(:disabled) {
background: #e5e7eb;
border-color: #9ca3af;
}
.load-more-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.load-more-button__spinner {
width: 16px;
height: 16px;
border: 2px solid #d1d5db;
border-top: 2px solid #374151;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
}
.loading-spinner__spinner {
width: 40px;
height: 40px;
border: 3px solid #e5e7eb;
border-top: 3px solid #2563eb;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-spinner__message {
color: #6b7280;
font-size: 1rem;
margin: 0;
}
.error-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
text-align: center;
}
.error-message__icon {
font-size: 3rem;
}
.error-message__title {
font-size: 1.5rem;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.error-message__text {
color: #6b7280;
margin: 0;
}
.error-message__retry-button {
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.error-message__retry-button:hover {
background: #1d4ed8;
}
/* Responsive Design */
@media (max-width: 768px) {
.job-board {
padding: 1rem;
}
.job-board__title {
font-size: 2rem;
}
.job-card {
padding: 1rem;
}
.job-card__metadata {
flex-direction: column;
gap: 0.25rem;
}
}Advanced Techniques
API Integration Best Practices
Error Handling Strategy:
- Network Errors: Handle fetch failures gracefully
- Invalid Data: Validate API responses before processing
- Rate Limiting: Implement retry logic with exponential backoff
- Timeout Handling: Set reasonable timeouts for API calls
Performance Optimization:
- Parallel Fetching: Use
Promise.all()for multiple API calls - Caching: Cache job IDs to avoid refetching
- Debouncing: Prevent rapid successive API calls
- Request Cancellation: Cancel ongoing requests when component unmounts
Data Validation:
// Example validation approach
const validateJobData = (data: any): Job | null => {
if (!data || typeof data !== 'object') return null;
const { id, title, by, time, type, url } = data;
if (
typeof id !== 'number' ||
typeof title !== 'string' ||
typeof by !== 'string' ||
typeof time !== 'number' ||
type !== 'job'
) {
return null;
}
return {
id,
title,
by,
time,
url: typeof url === 'string' ? url : undefined
};
};State Management Patterns
Loading States:
- Initial Loading: Show spinner when first fetching job IDs
- Pagination Loading: Show loading state for "Load More" button
- Individual Job Loading: Handle cases where job details fail to load
- Error Recovery: Allow retrying failed requests
Data Caching:
// Example caching strategy
const useJobCache = () => {
const [cache, setCache] = useState<Map<number, Job>>(new Map());
const getCachedJob = useCallback((id: number) => {
return cache.get(id);
}, [cache]);
const setCachedJob = useCallback((job: Job) => {
setCache(prev => new Map(prev).set(job.id, job));
}, []);
return { getCachedJob, setCachedJob };
};Performance Optimization
Virtual Scrolling:
- For large job lists, implement virtual scrolling
- Only render visible job cards
- Reuse DOM elements for better performance
Memoization:
// Example memoization
const MemoizedJobCard = React.memo(JobCard, (prevProps, nextProps) => {
return prevProps.job.id === nextProps.job.id;
});
const formatDate = useCallback((timestamp: number): string => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}, []);Common Pitfalls and Solutions
API Integration Issues
Problem: Jobs not loading or displaying incorrectly Solution:
- Validate API responses before processing
- Handle cases where job details are missing
- Implement proper error boundaries
- Add retry logic for failed requests
Problem: Performance issues with large job lists Solution:
- Implement virtual scrolling for large lists
- Cache job data to avoid refetching
- Use
React.memofor job card components - Optimize re-renders with proper dependencies
State Management Issues
Problem: Jobs disappearing or duplicating Solution:
- Use unique keys for job cards (job ID)
- Properly manage state updates
- Avoid mutating existing state
- Implement proper loading state management
Problem: "Load More" button not working correctly Solution:
- Track current page and loaded jobs properly
- Calculate remaining jobs correctly
- Handle edge cases where API returns fewer jobs
- Disable button during loading
Performance Issues
Problem: Slow loading or unresponsive UI Solution:
- Implement proper loading states
- Use
useCallbackanduseMemofor expensive operations - Optimize API calls with parallel fetching
- Add proper error boundaries
Problem: Memory leaks from unmounted components Solution:
- Cancel ongoing API requests on unmount
- Clean up event listeners and timers
- Use AbortController for fetch requests
- Implement proper cleanup in useEffect
Styling Best Practices
CSS Architecture
Component-Scoped Styles:
- Use CSS modules or styled-components
- Avoid global styles that might conflict
- Implement consistent naming conventions
Responsive Design:
- Use relative units (rem, em, %) instead of pixels
- Implement mobile-first design approach
- Test on various screen sizes and devices
Accessibility:
- Maintain sufficient color contrast
- Provide focus indicators for interactive elements
- Use semantic HTML elements
- Support screen readers with proper ARIA labels
Animation Guidelines
Loading Animations:
/* Smooth loading spinner */
.loading-spinner {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}Hover Effects:
/* Subtle hover effects */
.job-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.job-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}Next Steps and Enhancements
Advanced Features to Consider
Search and Filtering:
- Search by Job Title: Filter jobs by keywords
- Filter by Date: Show jobs from specific time periods
- Filter by Company: Group jobs by posting author
- Advanced Search: Multiple criteria filtering
User Experience Enhancements:
- Job Bookmarks: Allow users to save interesting jobs
- Job Alerts: Notify users of new matching jobs
- Job Sharing: Share job postings on social media
- Job Application Tracking: Track applied jobs
Key Takeaways
What You've Learned
- API Integration: How to work with external APIs and handle responses
- State Management: Managing complex state with loading, error, and data states
- Pagination: Implementing efficient "Load More" functionality
- Error Handling: Graceful handling of network failures and invalid data
- User Experience: Creating responsive and accessible job listings
Skills You've Developed
- React Patterns: Custom hooks, component composition, state management
- API Design: RESTful API integration, error handling, data validation
- Performance: Optimizing API calls, caching, and rendering
- Accessibility: ARIA labels, keyboard navigation, screen reader support
Common Mistakes to Avoid
❌ Poor Error Handling: Not handling API failures or invalid data ❌ Performance Issues: Fetching all jobs at once or not caching data ❌ Accessibility Issues: Missing ARIA labels or keyboard support ❌ State Management: Not properly tracking loading and error states ❌ User Experience: No loading feedback or unclear error messages
Best Practices to Remember
✅ Start Simple: Build basic functionality before adding advanced features ✅ Handle Errors: Implement proper error boundaries and retry logic ✅ Think Performance: Optimize API calls and rendering ✅ Test Thoroughly: Test on different devices and network conditions ✅ Document Everything: Clear API documentation and usage examples
This comprehensive guide has equipped you with the knowledge and skills to create professional-grade job board components. Remember, the key to success is starting simple, handling errors gracefully, and focusing on user experience. Your job board will now provide users with a smooth, accessible, and efficient way to discover career opportunities!
High-Performance Infinite Scroll Component
Master machine coding by creating an infinite scroll component with virtualization, intersection observer, and advanced performance optimization techniques.
Memory Game with React
Master machine coding by building a card memory game with React, implementing game logic, animations, and state management.