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!
Let’s take a closer look at our weapon of choice, the tech stack that we’ll use for this project.
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.
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:
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.
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:
Now, let’s install Chakra UI in the same project using the command below:
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:
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.
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:
<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:
And on a mobile browser:
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, }
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.
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.
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:
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.
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>
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.
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:
And on mobile:
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:
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.
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!
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.js 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
2 Replies to "Hacker News client with Chakra UI and Next.js "
Its a copy of https://github.com/kovacsmarkakos/hacker-news-next which i think has some better styling but using chakra like this is a good example of its features. It’s totally impossible to do the tutorial in this state. the github page is also lacking code. But I guess you can guess what to write.
Hi Stephanie, thanks for the comment. Please check this repository that is linked with the article: https://github.com/kokanek/firebase-react-notifications
It has all the necessary code updated to its latest version. That should help you get along with the tutorial.