Kapeel Kokane Coder by day, content creator by night, learner at heart!

Hacker News client with Chakra UI and Next.js 

8 min read 2291

Next Chakra Hackernews

If you’re interested in entrepreneurship and computer science, and you actively keep up with the latest news around those topics, you might already be familiar with Hacker News. For those who aren’t, Hacker News is a social news website run by the investment fund Y Combinator.

In my opinion, while the quality of posts that the site publishes is debatable, the UI seems quite outdated. Don’t get me wrong, it’s still a decent UI that is crazy fast, but it doesn’t seem polished enough to compete with websites in the year 2022.

Some argue that this is by design. The site is built on a tech stack as close to pure HTML, CSS, and JavaScript as possible to avoid the bundle size and other complexities of a UI framework.

But, with the advent of breakthrough technologies like Next.js, it’s possible to get closer to that level of performance despite using a UI framework. In this article, we’ll do just that by building a clone of the Hacker News client using Chakra UI and Next.js. Let’s get started!

Table of contents

Tech stack

Let’s take a closer look at our weapon of choice, the tech stack that we’ll use for this project.

UI framework: Next.js

As stated earlier, our UI framework of choice will be Next.js because we want to leverage server-side rendering, which Next.js supports out of the box. Apart from that, we’ll also indirectly benefit from other features like file-system based routing, code splitting, fast refresh, and more.

Along with Next.js, we’ll use Chakra UI for the component library. Chakra is an amazing UI library that provides modern-looking React components that you can customize without writing a single line of CSS. The library also features responsive design support out of the box.

Backend API

To query the latest items that we need to display in our app, we’ll make a call to the free Hacker News APIs. Basically, we need to call the following two APIs:

To implement the backend, make a call to the first API and fetch the IDs of all 500 items. A call to https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty returns the following code:

[
 30615959,
  30634872,
  30638542,
  30638590,
  30635426,
  30637403,
  30638830,
  30632952,
  ...
]

We fix a page size of 20 items and determine which IDs fall under that page using the formula below:

We made a custom demo for .
No really. Click here to check it out.

pageStartIndex = Number(page)*Number(pagesize)
pageEndIndex = (Number(page)+1)*Number(pagesize) - 1

Once we have the indices, we’ll trigger an API call to fetch the details of all 20 items within that range of indices in parallel. When you call for one item, the API URL https://hacker-news.firebaseio.com/v0/item/30615959.json?print=pretty returns the following:

{
  by: "rayrag",
  descendants: 50,
  id: 30615959,
  kids: [
    30637759,
    30639031,
    30637901,
    30637711,
    ...
  ],
  score: 364,
  time: 1646841853,
  title: "Pockit: A tiny, powerful, modular computer
",
  type: "story",
  url: "https://www.youtube.com/watch?v=b3F9OtH2Xx4"
}

All this will happen on the server-side due to the magic of Next.js. We’ll only get the necessary details to populate the 20 items on the UI. Once the user clicks on any of the list items, we’ll navigate to the URL of that item in a new tab in the browser.

Building the UX

Project setup

The frontend setup is as easy as creating a new Next.js repo, which we can do using the create-next-app command. Navigate to a folder where you want to create the project and run the following command:

npx create-next-app hackernews

Next.js will take care of the rest. After the script has completed running, there will be a new folder created with the name hackernews. Navigate into it and start the application to see the welcome screen:

cd hackernews
yarn dev

The code above will bring up the familiar start page for Next.js projects:

Nextjs Homepage

Chakra UI integration

Now, let’s install Chakra UI in the same project using the command below:

npm i @chakra-ui/react @emotion/[email protected]^11 @emotion/[email protected]^11 [email protected]^6

Once Chakra UI is installed, we need to go to pages/_app.js and wrap the root with ChakraProvider so that it looks like the following code:

import { ChakraProvider } from "@chakra-ui/react";
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  )
}
export default MyApp

Now, we’re all set to use Chakra UI in our project. You can follow along and build it yourself or refer to this GitHub repository.

Home page

Next, we’ll modify the pages/index.js file. We’ll use Chakra UI components to build the site title and the main header along with the pagination menu. We’ll have just have two styles in our app. For one, a .container will position our main site in the center, and a .main style will hold our entire site UI.

Then, we create our header and title component as follows. The menu is hardcoded for now but will be changed later:

&lt;Box className={styles.main} px={[4, 10]}>
  <Heading as='h1' size='4xl'>
    Hacker <span style={{color: 'teal'}}>news</span>
  </Heading>
  <Flex direction="row" justify='space-between' align='center' width="100%" mt="12">
   <Heading as='h1' size='xl'>
      Top news
    </Heading>
    <Menu>
      <MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
        Page
      </MenuButton>
      <MenuList>
        <MenuItem>1</MenuItem>
        <MenuItem>2</MenuItem>
       <MenuItem>3</MenuItem>
       <MenuItem>4</MenuItem>
       <MenuItem>5</MenuItem>
     </MenuList>
    </Menu>
  </Flex> 
</Box>

Here’s what the output looks like on the desktop browser:

Hacker News Desktop Output

And on a mobile browser:

Hacker News Mobile Output

List UI with dummy data

Next, we’ll create a component to display the Hacker News list items. We need to show the title, the upvotes, comments, and the user who posted the item. Before integrating the API, we’ll assume some dummy values for these and create the UI for the item.

Create a components folder and a file called ListItem.jsx, which will hold the presentation code for the list items. To keep the list item responsive, we’ll use the Flex component provided by Chakra UI to build several Flexbox rows and columns.

The component looks like the following code:

export default function ListItem({ item }) {
  return (
    <Flex direction="row" align={"center"} mb={4}>
      <Flex style={{ flex: 2 }} justify="center" mt={-8}>
        <Tag
          size={"md"}
          key={"md"}
          borderRadius='full'
         variant='solid'
          colorScheme='teal'
        >
          <TagLabel>{item.index}</TagLabel>
        </Tag>
      </Flex>
      <div style={{ flex: 12 }}>
        <Flex direction={"column"}>
          <Heading as='h1' size='sm'>
            {item.heading}
          </Heading>
          <Flex direction={"row"} justify="space-between" mt="2" wrap={"wrap"}>
            <Text fontSize='sm' color="gray.500" >{item.site}</Text>
            <Text fontSize='sm'>{item.time} - by <span style={{ color: '#2b6cb0' }}>{item.user}</span> </Text>
          </Flex>
          <Flex direction="row">
            <Button leftIcon={<ArrowUpIcon />} colorScheme='blue' variant='ghost'>
              {item.likes}
            </Button>
         <Button leftIcon={<ChatIcon />} colorScheme='orange' variant='ghost'>
              {item.comments}
            </Button>
          </Flex>
        </Flex>
      </div>
    </Flex>
  )
}

Let’s hardcode a single JSON item for testing purposes. We’ll check how it gets displayed on the UI with the ListItem component we developed just now:

const item = {
  heading: "Can't you just right click on this?",
  site: "lucasoftware.com",
  time: "10h",
  user: "bangonkeyboard",
  likes: 20,
  comments: 50,
  index: 1,
}

JSON Testing Purposes

The only functionality that this ListItem code is missing is the redirection to a new tab when any of the items are clicked. We’ll add that later. Now, all we need to do is fetch the list of items from the backend and map over it to create the list items.

API integration

Next, we’ll add API integration. We’ll make a call to the Top Stories API that we discussed earlier, then fetch the details for the items based on the page that the user is on. The page number can be read from the query param in the URL.

Server-side rendering

To make all this happen on the server-side, we’ll use the getServerSideProps method that Next.js provides. All the code that is written inside of that method is executed on the server-side, and the data returned from that method is supplied to the React component as props:

Server Side Rendering Data Fetching

The code below goes inside the getServerSideProps method and fetches the posts:

export async function getServerSideProps(context) {
  let pagesize = PAGE_SIZE;
  let { page=1 } = context.query;
  let posts = await fetchAllWithCache(API_URL);

  page = page == 0 ? 0 : page - 1;
  const slicedPosts = posts.slice(Number(page)*Number(pagesize), (Number(page)+1)*Number(pagesize));

  const jsonArticles = slicedPosts.map(async function(post) {
  return await fetchAllWithCache(`https://hacker-news.firebaseio.com/v0/item/${post}.json?print=pretty`);
  });

  const returnedData = await Promise.all(jsonArticles);
  return {
    props: {
       values: returnedData,
       totalPosts: posts.length
    }
  }
}

Notice that the page number is being read on the server-side using the page query param that is made available by context param. Then, the results are sliced, and the details are fetched for the sliced post IDs.

We’ve also introduced a caching layer using memory-cache so that all these APIs are cached on our server for 60 minutes. The caching logic is as follows:

async function fetchWithCache(url) {
  const value = cacheData.get(url);
  if (value) {
    return value;
  } else {
    const minutesToCache = 60;
    const res = await fetch(url);
    const data = await res.json();
    cacheData.put(url, data, minutesToCache * 1000 * 60);
    return data;
  }
}

At the end of this method, we have the posts being passed to the React component as props.

Iterate over the list

Next, we iterate over the list that we get in props and call the ListItem component:

<Flex direction="column" width="100%" mt="8">
  {posts.map((post, i) => <ListItem item={post} key={post.id} index={(page - 1)*PAGE_SIZE+i+1} />)}
</Flex>

Parsing the domain

There are two more things that we need to take care of. The Hacker News API does not return us the domain name separately, so we need to extract it out of the URL ourselves. A neat trick is to use the URL helper as follows:

const { hostname } = new URL(item.url || 'https://news.ycombinator.com');

The code above helps us extract the hostname, or the domain name out of any URL. Otherwise, it will always be set to hackernews.com to prevent the app from crashing.

Parsing the time

Additionally, we need a utility to convert the timestamp that we get back into a human-readable time. For instance, the 1647177253 that we get as the value of time needs to be converted into 3 hours ago. It sounds tricky but is actually quite straightforward.

The utility function below accomplishes just that. First, it calculates the number of seconds that have passed since that time stamp, then calculates the days, hours, minutes, and seconds that have passed sequentially and returns when a non-zero value is found:

export function getElapsedTime(date) {
  // get total seconds between the times
  var delta = Math.abs(new Date().getTime()/1000 - date);
  // calculate (and subtract) whole days
  var days = Math.floor(delta / 86400);
if (days) return `${days} days ago`;
  delta -= days * 86400;
  // calculate (and subtract) whole hours
  var hours = Math.floor(delta / 3600) % 24;
 if (hours) return `${hours} hours ago`;
  delta -= hours * 3600;
  // calculate (and subtract) whole minutes
  var minutes = Math.floor(delta / 60) % 60;
 if (minutes) return `${minutes} minutes ago`;
  delta -= minutes * 60;
  // what's left is seconds
  var seconds = delta % 60;
 return `${seconds} seconds ago`;
}

When we refresh the page, the code above generates a beautiful UI with 20 items populated. On our desktop, it looks like the following image:

Hacker News Generated UI

And on mobile:

Mobile UI Hacker News API

Pagination

Lastly, we need to support pagination, which we’ll do in two ways. For one, we’ll create a Load more button at the bottom of the page, which, when clicked, will load the next bunch of stories by redirecting to the next page number.

Secondly, we’ll have the page dropdown, which can be used to directly select the page we need to visit. All we need to do is load the correct route when a particular page is selected. The button looks something like this:

Button Page Dropdown

Below is the elegant code that loads more posts:

const onLoadMore = () => {
 router.push(`?page=${Number(page)+1}`)
}

Now, we just create a menu with 25 numbers and call the same function with the page number when the menu item is clicked:

<MenuList>
  { Array.from(Array(25).keys()).map(item => <MenuItem key={item} onClick={() => onLoadMore(item+1)}>{item+1}</MenuItem>)}
</MenuList>

And that takes care of the page navigation to the different pages.

Conclusion

With that in place, our app is complete! In this article, we’ve built a Hacker News client that is responsive for mobile view, is server-side rendered, and supports pagination.

Building this type of real-world application teaches us some important lessons and tricks, like the one we used to parse the URL, or the subroutine that we used for time conversion.

Give it a try and build your own version of a Hacker News client using the free APIs and the UI framework of your choice. I hope you enjoyed this article, happy coding!

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Kapeel Kokane Coder by day, content creator by night, learner at heart!

2 Replies to “Hacker News client with Chakra UI and Next.js ”

Leave a Reply