How to Build a Hacker News Clone Using React

Hacker News is a popular social news website focusing on computer science and entrepreneurship, created by startup incubator Y Combinator. The site allows users to submit and vote on articles, with the most popular ones rising to the top.

In this in-depth tutorial, we‘ll walk through building a clone of Hacker News from scratch using React. You‘ll learn how to fetch data from the Hacker News API, render it in a clean web interface, and implement essential features like viewing top stories, newest submissions, and best articles.

By the end, you‘ll have a solid grasp of core React concepts and patterns like components, props, state management with hooks, and client-side routing. Let‘s jump right in!

Hacker News API Overview

First, let‘s take a look at the official Hacker News API we‘ll be using to fetch story data. The API provides various endpoints for retrieving different types of stories:

Each of these endpoints returns an array of story IDs, not the actual story details. To fetch the data for a specific story, we need to make a separate API call using its ID:

https://hacker-news.firebaseio.com/v0/item/[story_id].json

For example, to get the details for story 123456, we‘d call:

https://hacker-news.firebaseio.com/v0/item/123456.json

The story details include properties like:

  • id: The story‘s unique ID
  • by: The username of the submitter
  • title: The title of the story
  • url: The URL of the story
  • time: Creation date of the story, in Unix time
  • score: The story‘s score (upvotes – downvotes)
  • descendants: The total number of comments

Setting Up a New React Project

With a basic understanding of the Hacker News API, let‘s set up a new React project for our app. We‘ll use the create-react-app tool to generate a project boilerplate.

Open your terminal and run:

npx create-react-app hackernews-clone

Once the project is created, navigate into the new directory:

cd hackernews-clone

Next, install a few additional dependencies we‘ll be using:

npm install axios react-router-dom
  • axios: A promise-based HTTP client we‘ll use to make API requests
  • react-router-dom: A collection of navigational components for React

For styling, we‘ll use Sass, a CSS extension language. To set it up, rename the src/App.css file to src/App.scss and update the import statement in src/App.js:

import ‘./App.scss‘;

Now we‘re ready to start building out the main components and pages of our app. But first, let‘s organize our project structure a bit. Inside the src directory, create the following folders:

  • components: For reusable UI components
  • pages: For top-level page components like HomePage, ShowStories, etc.
  • api: For modules related to data fetching and API integration

Creating the Header and HomePage Components

Every app needs a header, so let‘s build that first. Create a new file src/components/Header.js with the following code:

import { Link } from ‘react-router-dom‘;

function Header() {
  return (
    <div className="header">
      <div className="header-title">
        <Link to="/">Hacker News Clone</Link>  
      </div>
      <div className="header-nav">
        <Link to="/top">Top</Link>
        <Link to="/new">New</Link>
        <Link to="/best">Best</Link>    
      </div>
    </div>
  );
}

export default Header;

Here we define a simple Header component with a title that links to the homepage, and navigation links to view top, new, and best stories.

The Link component is imported from react-router-dom and provides declarative routing within the app. It‘s similar to using anchor tags, but prevents the default page reload behavior, allowing the app to update the UI instantly when navigating between pages.

Next, create src/pages/HomePage.js for the main landing page:

function HomePage() {
  return (
    <>

      <p>
        Your one-stop destination for the latest and greatest in tech news, startup launches, 
        Show HN demos, and thoughtful discussion.  
      </p>
    </>
  );  
}

export default HomePage;

Nothing fancy yet, just a placeholder component with a welcome message. The empty tags <></> are shorthand fragments that let you group multiple elements without adding an extra node to the DOM.

Implementing Basic Routing

With some initial components in place, let‘s set up basic routing so we can navigate between the homepage and different story feeds.

Update src/App.js to the following:

import { BrowserRouter as Router, Switch, Route } from ‘react-router-dom‘;
import Header from ‘./components/Header‘;
import HomePage from ‘./pages/HomePage‘;
import ShowStories from ‘./pages/ShowStories‘;

function App() {
  return (
    <Router>
      <div className="App">
        <Header />

        <Switch>
          <Route path="/" exact>
            <HomePage />
          </Route>
          <Route path="/:type">
            <ShowStories /> 
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

export default App;

After importing the necessary routing components and pages, we wrap the entire app in a Router. This enables client-side routing throughout the component tree.

Inside the Router, we define two routes using the Route component:

  1. The homepage route / renders the HomePage component. The exact prop ensures it only matches exactly /, not routes that start with a /.

  2. The /:type route renders the ShowStories component, which we‘ll create next to fetch and display stories from the different Hacker News feeds. The :type part is a URL parameter that will be either top, new, or best.

The Switch component wraps the routes and ensures that only the first matching route is rendered. Routes are matched top-down, so more specific routes should be listed before less specific ones.

Create a new file src/pages/ShowStories.js with a placeholder for now:

function ShowStories() {
  return <h2>Stories</h2>;
}

export default ShowStories;

If you run the app with npm start and click the nav links, you‘ll see the routes working, but only showing the simple components we‘ve built so far. Next up: fetching live data from the Hacker News API!

Fetching Data with React Hooks

To fetch data in a function component, we can use the useState and useEffect hooks from React. Hooks let you "hook into" state and lifecycle features without writing a class component.

First, create a reusable custom hook to fetch story data given an API endpoint. In src/api/fetchStories.js:

import { useState, useEffect } from ‘react‘;
import axios from ‘axios‘;

function fetchStories(type) {
  const [stories, setStories] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(true);

    const endpoint = `https://hacker-news.firebaseio.com/v0/${type}stories.json`;

    axios.get(endpoint)
      .then(response => {
        const storyIds = response.data;

        const storyPromises = storyIds.slice(0, 30).map(fetchStory);
        return Promise.all(storyPromises);
      })
      .then(stories => {
        setStories(stories);
        setIsLoading(false);
      })
      .catch(() => setIsLoading(false));

  }, [type]);

  return { isLoading, stories };
}

function fetchStory(id) {  
  return axios
    .get(`https://hacker-news.firebaseio.com/v0/item/${id}.json`)
    .then(response => response.data);
}

export default useDataFetcher;

This hook takes a type parameter that determines which feed to fetch (top, new or best stories). Inside the hook:

  1. We initialize state variables for the fetched stories and loading status using the useState hook.

  2. The useEffect hook is used to fetch data when the component mounts and whenever the type changes. The type is specified in the dependency array as the second argument.

  3. First, we fetch the array of story IDs from the appropriate endpoint using axios.get().

  4. Then we fetch the individual story details by mapping over the IDs with the fetchStory helper function and using Promise.all to wait for all the async requests to complete.

  5. Once the story data is fetched, we update the stories state variable and set isLoading to false. The catch block handles any errors.

  6. Finally, the hook returns an object with the isLoading status and fetched stories. This allows any component using this hook to access the data and loading state.

Now let‘s use this hook in the ShowStories page component to display the fetched stories. Update src/pages/ShowStories.js:

import Story from ‘../components/Story‘;
import fetchStories from ‘../api/fetchStories‘;

function ShowStories({ match }) {
  const type = match.params.type;
  const { isLoading, stories } = fetchStories(type);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  return (
    <>
      {stories.map(story => (
        <Story key={story.id} story={story} />
      ))}
    </>
  );
}

export default ShowStories;

The ShowStories component receives a match prop from React Router, which contains the matched route params. We extract the type parameter to determine which feed to display.

By calling the fetchStories hook with type, we get back the loading status and fetched stories. While isLoading is true, we display a loading message. Once the stories are fetched, we map over the array and render a Story component for each one. The key prop is required when rendering a list of components to give each one a stable identity.

Finally, create src/components/Story.js to display a single story item:

function Story({ story }) {
  const { id, by, title, url, time } = story;

  return (
    <div className="story">
      <div className="story-title">
        <a href={url}>{title}</a>
      </div>
      <div className="story-meta">        
        <span>{by}</span>
        <span>{new Date(time * 1000).toLocaleString()}</span>
      </div>
    </div>
  );
}

export default Story;

This component receives the story object as a prop, then extracts and displays relevant details like the title (linked to the article URL), author, and creation time. The time property from the API is in Unix time (seconds since epoch), so we multiply by 1000 to convert to milliseconds before formatting with toLocaleString().

Deployment

Once you run the application you will see the list of top stories displayed using our react component. To let the rest of the world see your amazing creation, simply create a new GitHub repo and push your code up to GitHub. Then visit the Vercel website and select "New Project". Select "From GitHub" and connect your GitHub account to allow Vercel access to your repos. Select your "Hackernews-Clone" repository and click deploy!

Wrapping Up and Next Steps

Congratulations, you now have a functional Hacker News clone built with React! In this tutorial, we covered:

  • Fetching data from the Hacker News API using axios
  • Rendering lists of stories with React components
  • Reactive state management with the useState hook
  • Side effects and data fetching with the useEffect hook
  • Reusable data fetching logic with custom hooks
  • Client-side routing with React Router

There are plenty of additional features you could add to extend this app:

  • Pagination to fetch and display more stories
  • Sorting options to view stories by popularity, date, etc.
  • Allowing users to favorite stories and save them for later
  • Hosting the production app on a service like Vercel

I hope this tutorial helped you feel more comfortable working with React, APIs, and core tools in the ecosystem. Check out the complete source code and try deploying your own copy of the app. Happy coding!

Similar Posts