How to Use Recursive Components in React to Display Deeply Nested Data

This doesn’t have to be a nightmare to build

I’m sure you’ve experienced the sudden nightmare when you make an API call and the data that gets returned contains an array in it with more data. Then you open that array and it’s filled with more arrays with more data. And then each one of those data elements is another array with more data!

Here’s the Pathetic News Network app from the picture above.

Here’s an example of the data we’re working with. It’s my project from last week to display articles and comments from the Hacker News API.

That link will display all the comments for one article. As you can see in the “children” array, each comment has another “children” array with comments under it that leads to another “children” array of comments for that comment, and so on.

Visual Representation of HN API

That doesn’t have to make you crazy. In fact, it should make you happy to find your data in a set like that; very standardized, each sub-element using the same data structure as the element above it, and no extra API calls to get more data, because it’s all in one result!

Here’s another example. Microsoft ended up cutting off this data, but this is (was) the Bing API for Covid-19 data that they released a few months ago:

Still hurts that they cut off the feed

It starts with “areas” being countries, then there’s an “areas” array under that for states, then another “areas” array for counties, etc… It was scary at first, but then fun and inviting once you get to playing with it using recursion.

Which is what we’re going to do now using the Hacker News API.

Here’s the component for each article:

// this is each news article
class NewsItem extends Component {
render() {let baseData = this.props.newsData; // just got tired of typing that a lotreturn(
<div style={{marginLeft:'10px'}}>
{/* I had to check if there was a title, because some of the articles HAVE NO TITLE?!? */}{baseData.title &&
<div>
<a style={{textDecoration: 'none'}} href={baseData.url} target="blank">
<h1 className="articles" style={{marginBottom:'0px'}}>{baseData.title}</h1>
</a>
<NewsInfo newsData={this.props.newsData} />
</div>
}
</div>
)
}
}

That just prints the list of article titles from the main API call and calls a component, NewsInfo to list the specifics.

So in this picture it’s just the line that says “Show HN: Learn coding to become a software engineer”

Then the NewsInfo component will display the “Created 06–04–20…” line below it. Here’s that component:

// the detail line that goes under the article titleclass NewsInfo extends Component {constructor(props) {super(props);this.state = {postAuthor: '',loadingComments: false,comments: [],pageNumber: 0,showComments: false};}showComments = () => {let commentsShowing = this.state.showComments;this.setState({showComments: !this.state.showComments})if(!commentsShowing) this.fetchComments();}// calls the datafetchComments = () => {this.setState({loadingComments:true});// query url is the url for whatever api endpoint we needlet commentsUrl = `//hn.algolia.com/api/v1/items/${this.props.newsData.objectID}`;// switched to axios to avoid CORS errorsaxios.get(commentsUrl).then(data => {// check if there's dataif (data.data.children) {// if there is, add it to the data we already havethis.setState({loadingComments: false,postAuthor: data.data.author,comments: [...this.state.comments, ...data.data.children]})} else {console.log("no data")}})}render() {let baseData=this.props.newsDatareturn(<div id="commentsDiv" style={{marginLeft:'5px'}}><span style={{fontSize:'10pt'}}>Created: {moment(new Date(baseData.created_at)).format("MM-DD-YY hh:mm a")}&nbsp;Author: {baseData.author}&nbsp;<span onClick={this.showComments}>Comments: {baseData.num_comments ? baseData.num_comments  : 0 } <img src={commentsIcon} alt='' title='show/hide comments' style={{width:'20px'}}/></span>&nbsp;Points: {baseData.points}{this.state.loadingComments && <img src={loaderIcon} style={{width:'16px',marginLeft:'4px'}} alt='' />}</span>{this.state.showComments && this.state.comments.length>0 &&<div style={{marginTop:'10px',marginLeft:'40px'}}>{this.state.comments.map((comment) => <Comment postAuthor={this.state.postAuthor} key={comment.id} comment={comment} />)}</div>}</div>)}}

See how the word “comments” on that line is a link to expand or contract the actual comments for that article? That’s when it does the fetch to get the individual article and the “children” arrays under it.

It also displays another new component we’ve called “Comment” and this is the one we’ll be using recursion on to nest. In case you’re wondering why I’ve basically just glossed over all this stuff is because it’s just pretty standard React component building, nothing too fancy.

Here’s what we came to see (only 600 words into the article):

function Comment({ comment,postAuthor }) {const [showChildren,setShowChildren] = useState(true);// this causes the data to check if there are more "children" comments under
// the current comment. If there are then is recursively renders more of this
// same component below the one we originally called and if not renders nothing
const nestedComments = (comment.children || []).map(comment => {return <Comment key={comment.id} postAuthor={postAuthor} comment={comment} type="child" />})return (// this margin causes the recursive nested comments to indent so
// the user can see the thread
<div style={{"marginLeft": "45px",marginBottom:'10px'}}><div style={{display:'inline',fontWeight:'bold',fontSize:'large'}} onClick={()=> setShowChildren(!showChildren)}>{showChildren ? '-' : '+'}<img src={`https://robohash.org/${comment.author}.png`} style={{width:'30px',marginRight:'4px',verticalAlign:'middle'}} title='' alt=''></img><span style={{fontWeight:'bold'}}>{comment.author}</span> <span style={{fontSize:'10pt'}}>{moment(new Date(comment.created_at)).format("MM-DD-YY hh:mm a")}</span>{comment.author===postAuthor && <img src={opIcon} alt='' title='Original Author' style={{width:'16px',marginLeft:'4px'}} />}</div>{/* this left border is the line that connects the comments on the same level in the thread */}
{showChildren &&
<div style={{"marginTop": "2px",borderLeft:'2px solid #cadbce',marginLeft:'4px',position:'relative'}}>
{/* this next line is the invisible div next to the left border that will collapse the comment thread when clicked */}
<div style={{width:'15px',float:'left',position:'absolute',top:'0',bottom:'0'}} onClick={()=>{setShowChildren(!showChildren)}} />
{/* outputs the comment text in the HTML format in which it was saved. this is the main comment */}
<div className="commentDiv" dangerouslySetInnerHTML={{ __html: comment.text }} />
{/* display any nested comments */}
{nestedComments}
</div>
}
</div>
)
}

What’s this diminutive, elegant sum-b doing that’s so great? Well, it’s checking to see if there are more comments underneath the current comment, and if there are, then it calls itself to output those comments as well using the same component and same comment structure as the one that called it the first time!

How’s that for a Happy Ending?

Let’s see how it works!

The first thing it’s doing is taking in two parameters as props,

function Comment({ comment,postAuthor }) {

The “comment” prop comes from the fetch we just did to get all comments for the article, and the “postAuthor” prop we’re passing from the original article. It’s specifically so that if the commentor happens to be the person who wrote the article, we can highlight them in some way. In our example we use a pencil icon next to their info line.

Here’s the recursion:

const [showChildren,setShowChildren] = useState(true);// this causes the data to check if there are more "children" comments under
// the current comment. If there are then is recursively renders more of this
// same component below the one we originally called and if not renders nothing
const nestedComments = (comment.children || []).map(comment => {return <Comment key={comment.id} postAuthor={postAuthor} comment={comment} type="child" />})

We made this a function component just because it doesn’t really do anything. There’s no real reason to use a state object, but we are going to want to add a toggle so that the user can choose to show or hide each comment if they like.

To do that, we use a “State Hook” to emulate a state object without actually having to create one.

const [showChildren,setShowChildren] = useState(true);

We’re setting a boolean variable called “showChildren” and defining a method called “setShowChildren” that we’ll use to manipulate it. Then on the right side we’re initializing showChildren to “true” because we want to start out with all comments visible and let the user decide whether to hide them or not.

Then this part:

const nestedComments = (comment.children || []).map(comment => {return <Comment key={comment.id} postAuthor={postAuthor} comment={comment} type="child" />})

creates a variable called “nestedComments” that maps the “children” array in the API results to ..what?!..more of this same Comment component! It does that by sending it the same original postAuthor prop and a new “comment” prop that is the current “comment.children” element, which we have also called “comment”.

Still with me? Good! Because if it loops through again and finds another “children” array, it’s going to do the same thing! And it’s going to keep doing the same thing until there are no more children arrays and no more comments.

But we still haven’t rendered the top comment yet! That’s what this mess is:

return (// this margin causes the recursive nested comments to indent so
// the user can see the thread
<div style={{"marginLeft": "45px",marginBottom:'10px'}}><div style={{display:'inline',fontWeight:'bold',fontSize:'large'}} onClick={()=> setShowChildren(!showChildren)}>{showChildren ? '-' : '+'}<img src={`https://robohash.org/${comment.author}.png`} style={{width:'30px',marginRight:'4px',verticalAlign:'middle'}} title='' alt=''></img><span style={{fontWeight:'bold'}}>{comment.author}</span> <span style={{fontSize:'10pt'}}>{moment(new Date(comment.created_at)).format("MM-DD-YY hh:mm a")}</span>{comment.author===postAuthor && <img src={opIcon} alt='' title='Original Author' style={{width:'16px',marginLeft:'4px'}} />}</div>{/* this left border is the line that connects the comments on the same level in the thread */}
{showChildren &&
<div style={{"marginTop": "2px",borderLeft:'2px solid #cadbce',marginLeft:'4px',position:'relative'}}>
{/* this next line is the invisible div next to the left border that will collapse the comment thread when clicked */}
<div style={{width:'15px',float:'left',position:'absolute',top:'0',bottom:'0'}} onClick={()=>{setShowChildren(!showChildren)}} />
{/* outputs the comment text in the HTML format in which it was saved. this is the main comment */}
<div className="commentDiv" dangerouslySetInnerHTML={{ __html: comment.text }} />
{/* display any nested comments */}
{nestedComments}
</div>
}
</div>
)

Once again, we’re back to just regular react components. Notice how I’ve added an invisible div there to call the useState hook we setup earlier?

{/* this next line is the invisible div next to the left border that will collapse the comment thread when clicked */}
<div style={{width:'15px',float:'left',position:'absolute',top:'0',bottom:'0'}} onClick={()=>{setShowChildren(!showChildren)}} />

When you click that div it calls the useState method we named “setShowChildren” to toggle our “showChildren” boolean. It didn’t have to be a toggle, because once showChildren is false, then we won’t have an invisible div there to click any more.

That’s why you can see we provided the same functionality on the second line, when the user clicks the commentor’s info line.

Then, once the original comment has been displayed to the screen, we call the recursive variable we made to display under it any other comments that have been made to that comment:

{/* display any nested comments */}
{nestedComments}

That’s it! That’s how you do it.

The only other note of interest is how I added an ‘avatar’ to each commentor’s info line. I’m sure Hacker News members can have their own personal avatars, but since they’re not included in the API results I simply used the RoboHash API to create one based on their user name.

<img src={`https://robohash.org/${comment.author}.png`} />

All that styling is what I used to make sure nested comments indented well enough. All that can be moved to a css stylesheet or a css object if that’s what you’re into.

Here’s a live verion: https://patheticnews.herokuapp.com/

Here’s the github repo for the whole app: https://github.com/crashdaddy/NewsFeed

and here’s where you can pay for me some programming lessons!

Thenk yew

4a 75 73 74 20 61 6e 6f 74 68 65 72 20 63 6f 6d 70 75 74 65 72 20 6e 65 72 64 20 77 69 74 68 20 61 20 62 6c 6f 67