React: Recreating the Hacker News Comments Section

Zach
July 7th 2021

Recently I was inspired by the minimalist look and feel of the Hacker News comments section, and I wanted to try recreating it with React:

pippin
[-]
Hey Zach, love this article! Keep it up!
reply
gandalf
[-]
dammit Pippin, why on earth are you reading webdev articles? Dont you know we have a huge battle tomorrow?
reply
pippin
[+]
legolas
[-]
What a wonderful blog post Zach! Consider me subscribed.
reply
gimli
[-]
for god's sake Legolas, don't encourage the lad. The last thing middle earth needs is another JavaGrifter
reply
legolas
[+]
An interactive demo of what we'll be making in this post.

The goal of this post is to show you how to recreate the demo above, while allowing the comments section to be generated from any list of comments you give it, so long as they look somewhat like this:

const commentData = [
{
id: 1,
parentId: null,
text: 'Nice article! I love React!',
author: 'sarah',
children: null
},
{
id: 2,
parentId: 1,
text: 'I agree Sarah, it\'s a great article. However, I prefer Vue.',
author: 'john',
children: null
},
{
id: 4,
parentId: 2,
text: 'ah sh*t, here we go again..',
author: 'sarah',
children: null
},
{
id: 3,
parentId: null,
text: 'Cool article. You should put some funny gifs in it or something.',
author: 'kevin',
children: null
}
];

In other words, our solution will be able to recursively generate a tree of comments from a flat array of comments data, so long as they contain the right information to do so (such as parentId if the comment is a reply to an existing comment).

Note: you can ignore the null children property in each comment for now, but it will come in handy in a second.

But enough preamble - let's get started!

Step 1: Getting Started

For simplicity's sake, we'll keep all of our code together in one long-ish file, and use mostly inline styles:

const commentData = [
{
id: 1,
parentId: null,
text: 'Nice article! I love React!',
author: 'sarah',
children: null
},
{
id: 2,
parentId: 1,
text: 'I agree Sarah, it\'s a great article. However, I prefer Vue.',
author: 'john',
children: null
},
{
id: 4,
parentId: 2,
text: 'ah sh*t, here we go again..',
author: 'sarah',
children: null
},
{
id: 3,
parentId: null,
text: 'Cool article. You should put some funny gifs in it or something.',
author: 'kevin',
children: null
}
];
function App() {
return (
<div style={{ padding: '16px' }}>
{commentData.map(comment => {
return (
<div>
{comment.text}
</div>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

Pretty simple - so far just a basic React app mapping through an array of comments:

A screenshot of a Hacker News comment section example.

Now let's make sure the comments are sorted and nested nicely as a tree of replies, and not just a long, visually confusing list. To do this, we'll need to create a function that converts our flat array of comments into a comment tree.

Step 2: Generating the Comment Tree

Luckily for us, a quick search of "convert flat array to tree javascript" into Google leads us to a popular Stack Overflow question, and a top answer that seems to have exactly what we need!

Let's go ahead and add it to our code. We'll name this function createTree(), and also put it to use and console.log() the results to make sure it's working:

function createTree(list) {
var map = {},
node,
roots = [],
i;
for (i = 0; i < list.length; i += 1) {
map[list[i].id] = i; // initialize the map
list[i].children = []; // initialize the children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.parentId) {
// if you have dangling branches check that map[node.parentId] exists
list[map[node.parentId]].children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
const commentData = [
{
id: 1,
parentId: null,
text: 'Nice article! I love React!',
author: 'sarah',
children: null
},
{
id: 2,
parentId: 1,
text: 'I agree Sarah, it\'s a great article. However, I prefer Vue.',
author: 'john',
children: null
},
{
id: 4,
parentId: 2,
text: 'ah sh*t, here we go again..',
author: 'sarah',
children: null
},
{
id: 3,
parentId: null,
text: 'Cool article. You should put some funny gifs in it or something.',
author: 'kevin',
children: null
}
];
function App() {
const commentTree = createTree(commentData);
console.log(commentTree);
return (
<div style={{ padding: '16px' }}>
{commentData.map(comment => {
return (
<div>
{comment.text}
</div>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

Now peek in the console, and you should see an array containing some good news...

So our createTree() did its job perfectly, but just because we have a properly nested array of comments doesn't mean they're just going to magically appear that way in the UI for our users. Let's solve this in the next step.

Step 3: Creating the Comment Component

We're going to need a create a special <Comment /> component that knows how to handle this type of nested data, by not only rendering itself but also recursively rendering any child comments (replies) beneath it. Thankfully I was able to find a great article on rendering comments with nested children using React components, and they provide us with a great component to use. I'll go ahead and add into our code so you can see how it works:

function createTree(list) {
var map = {},
node,
roots = [],
i;
for (i = 0; i < list.length; i += 1) {
map[list[i].id] = i; // initialize the map
list[i].children = []; // initialize the children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.parentId) {
// if you have dangling branches check that map[node.parentId] exists
list[map[node.parentId]].children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
const commentData = [...]; // let's abbreviate this from now on
function Comment({ comment }) {
const nestedComments = (comment.children || []).map(comment => {
return <Comment key={comment.id} comment={comment} />
})
return (
<div style={{marginLeft: "25px", marginTop: "10px"}}>
<div style={{fontWeight: 700}}>{comment.author}</div>
<div>{comment.text}</div>
{nestedComments}
</div>
)
}
function App() {
const commentTree = createTree(commentData);
return (
<div style={{ padding: '16px' }}>
{commentTree.map(comment => { // notice we are now using `commentTree` and not `commentData`
return <Comment comment={comment} />
})}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

The <Comment /> component above is going to recursively add any comment replies below itself, while also being a great place to add some more detail and styling.

And just like that, our comments have really started to take shape:

Now all that's left is some styling and little bit of added functionality, which I'll show you how to get started on in the final step.

Step 4: Styling and Interactivity

To get our imitation as close as possible we can inspect the comments of any Hacker News post and discover that the font in use is Verdana and the background color is #F6F6EF. We can also find the different font sizes and colors of the different pieces of text and apply those as well.

But what about hiding/collapsing different parts of the comment tree? Or enabling new replies?

Let me show you how to do the former, and then finding out how to do the latter won't be too hard:

import { useState, useEffect } from 'react';
function createTree(list) {...} // don't forget to keep this function!
const commentData = [
{
id: 1,
parentId: null,
text: 'Nice article! I love React!',
author: 'sarah',
children: null,
expanded: true // new comment property!
},
{
id: 2,
parentId: 1,
text: 'I agree Sarah, it\'s a great article. However, I prefer Vue.',
author: 'john',
children: null,
expanded: true
},
{
id: 4,
parentId: 2,
text: 'ah sh*t, here we go again..',
author: 'sarah',
children: null,
expanded: true
},
{
id: 3,
parentId: null,
text: 'Cool article. You should put some funny gifs in it or something.',
author: 'kevin',
children: null,
expanded: true
}
];
function Comment({ comment, collapse }) {
const nestedComments = (comment.children || []).map(comment => {
return <Comment key={comment.id} comment={comment} />
})
return (
<div style={{marginLeft: "25px", marginTop: "10px"}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{fontWeight: 700}}>{comment.author}</div>
<div
onClick={() => collapse(comment.id)}
style={{
cursor: 'pointer',
marginLeft: '6px',
fontSize: '12px'
}}
>
{comment.expanded ? `[-]` : `[+]`}
</div>
</div>
{comment.expanded && (
<div>
<div>{comment.text}</div>
{nestedComments}
</div>
)}
</div>
}
function App() {
const [comments, setComments] = useState(commentData);
const [commentTree, setCommentTree] = useState(createTree(comments));
useEffect(() => {
setCommentTree(createTree(comments));
}, [comments]);
const handleCommentCollapse = (id) => {
const updatedComments = comments.map((c) => {
if (c.id === id) {
return {
...c,
expanded: !c.expanded,
};
} else return c;
});
setComments(updatedComments);
};
return (
<div style={{ padding: '16px' }}>
{commentTree.map(comment => { // notice we are now using `commentTree` and not `commentData`
return <Comment key={comment.id} comment={comment} collapse={handleCommentCollapse} />
})}
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

With the above code, users can now now hide or expand any comment and its children by clicking the little plus/minus button:

The way we've made this work is by using some state, a handler (handleCommentCollapse()), and some conditional rendering.

We had to move the commentData into the comments state so that they can be updated/edited. Changes to them are tracked using a single useEffect.

Using a similar strategy to the one above can allow you to make any changes to the comments and how they're displayed.

I'll leave you to decide how to do the rest of them, but if you'd like a hint on how to add reply functionality, it's by conditionally rendering a <textarea> when the user clicks "reply", capturing its text value (and parentId) when the user submits it, and sending it back up the chain so it can be added into the comments array via a handler function, immediately updating the comment tree and its display.

Just keep in mind that on a real forum website (or comments section) these comments would need to be stored in (and retrieved from) a database so that they actually persist. Otherwise they'll be erased each time the state refreshes.

Recommended Posts

HubSpot Forms in React: Submit a form using the HubSpot API

In this post you will learn how to submit HubSpot forms from your React website using the HubSpot API.

Read more

React: Recreating the Hacker News comments section

In this post we'll use React to recreate the Hacker News comment section using a recursively generated comment tree.

Read more