Hacker News client with Chakra UI and Next.js
Tags: nextjs
Published: 2022-04-14 16:02:18

**Written by [Kapeel Kokane](https://blog.logrocket.com/author/kapeelkokane/)✏️**

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](https://news.ycombinator.com/). For those who aren't, Hacker News is a social news website run by the investment fund [Y Combinator](https://www.ycombinator.com/).

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](#tech-stack)
    *   [UI framework: Next.js](#ui-framework-nextjs)
    *   [Backend API](#backend-api)
*   [Building the UX](#building-the-ux)
    *   [Project setup](#project-setup)
*   [Chakra UI integration](#chakra-ui-integration)
*   [Home page](#home-page)
*   [List UI with dummy data](#list-ui-with-dummy-data)
*   [API integration](#api-integration)
*   [Server-side rendering](#serverside-rendering)
*   [Iterate over the list](#iterate-over-the-list)
*   [Parsing the domain](#parsing-the-domain)
*   [Parsing the time](#parsing-the-time)
*   [Pagination](#pagination)
*   [Conclusion](#conclusion)

## 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](https://chakra-ui.com/) for the component library. [Chakra is an amazing UI library](https://blog.logrocket.com/advanced-techniques-chakra-ui/) 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](https://hackernews.api-docs.io/v0/overview/introduction). Basically, we need to call the following two APIs:

*   The [New and Top stories API](https://hackernews.api-docs.io/v0/live-data/new-and-top-stories) retrieves 500 of the top item IDs in JSON format
*   The [get item by ID API](https://hacker-news.firebaseio.com/v0/item/30637759.json?print=pretty) gets all the details of an item when provided an item ID

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:

```js
[
 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:

```js
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:

```js
{
  by: "rayrag",
  descendants: 50,
  id: 30615959,
  kids: [
    30637759,
    30639031,
    30637901,
    30637711,
    ...
  ],
  score: 364,
  time: 1646841853,
  title: "Pockit: A tiny, powerful, modular computer</pre>

<pre>",
  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:

```bash
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:

```js
cd hackernews
yarn dev
```

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

![Nextjs Homepage](https://blog.logrocket.com/wp-content/uploads/2022/04/next-js-homepage.png)

## Chakra UI integration

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

```bash
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^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:

```js
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](https://github.com/kokanek/hackernews-responsive).

## 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:

```js
&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](https://blog.logrocket.com/wp-content/uploads/2022/04/hacker-news-desktop-output.png)

And on a mobile browser:

![Hacker News Mobile Output](https://blog.logrocket.com/wp-content/uploads/2022/04/hacker-news-mobile-output.png)

## 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:

```js
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:

```json
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](https://blog.logrocket.com/wp-content/uploads/2022/04/json-testing-purposes.png)

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](https://blog.logrocket.com/wp-content/uploads/2022/04/server-side-rendering-data-fetching.png)

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

```js
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:

```js
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:

```js
<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:

```js
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:

```js
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](https://blog.logrocket.com/wp-content/uploads/2022/04/hacker-news-generated-ui.png)

And on mobile:

![Mobile UI Hacker News API](https://blog.logrocket.com/wp-content/uploads/2022/04/mobile-ui-hacker-news-api.png)

## 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](https://blog.logrocket.com/wp-content/uploads/2022/04/button-page-dropdown.png)

Below is the elegant code that loads more posts:

```js
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:

```js
<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](https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try [LogRocket](https://logrocket.com/signup). 

![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)

[LogRocket](https://logrocket.com/signup) 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 — [start monitoring for free](https://logrocket.com/signup).