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?

  1. Fast Loading: Users expect quick access to job information
  2. Clear Information: Job title, company, and posting date should be prominent
  3. Responsive Design: Works seamlessly on desktop, tablet, and mobile
  4. Error Handling: Graceful handling of API failures and network issues
  5. Accessibility: Screen readers can navigate and understand job listings
  6. 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.

  1. Create Basic Structure

    • Set up your main component structure
    • Add basic styling and layout
    • Implement static job display
  2. Add API Integration

    • Fetch job IDs from the API
    • Fetch individual job details
    • Handle basic error cases
  3. 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.

  1. Loading States

    • Show loading spinners during API calls
    • Disable buttons during loading
    • Provide user feedback
  2. Error Handling

    • Handle network failures gracefully
    • Show retry options
    • Provide meaningful error messages
  3. 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.

  1. User Experience

    • Smooth animations and transitions
    • Responsive design for all screen sizes
    • Accessibility improvements
  2. 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.memo for 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 useCallback and useMemo for 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

  1. API Integration: How to work with external APIs and handle responses
  2. State Management: Managing complex state with loading, error, and data states
  3. Pagination: Implementing efficient "Load More" functionality
  4. Error Handling: Graceful handling of network failures and invalid data
  5. 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!