≡ Menu

Building a robust comment section is essential for any dynamic content platform, but managing threaded (nested) replies can be tricky.

In this tutorial, we’ll walk through how to build a fully functional, nested comment system using React functional components and hooks.

We will focus on the front-end logic, using a flat array of data and the parentId field to create the recursive structure.

1. Defining the Comment Data Model

The most critical part of a nested comment system is the data structure. We use a flat array where each comment object contains a parentId.

  • parentId: null: This is a top-level comment.
  • parentId: [ID]: This is a reply to the comment with that ID.

data.js

Let’s start with our mock data file, which simulates the initial comments fetched from a database.

// data.js
export const initialComments = [
  // Top-level comments (parentId: null)
  { id: 1, userId: 'userA', content: 'This is the first top-level comment.', parentId: null, timestamp: Date.now() - 50000 },
  { id: 2, userId: 'userB', content: 'I agree, great article!', parentId: null, timestamp: Date.now() - 40000 },

  // Replies
  { id: 3, userId: 'userC', content: 'This is a reply to the first comment.', parentId: 1, timestamp: Date.now() - 30000 },
  { id: 4, userId: 'userA', content: 'My second reply to the first comment.', parentId: 1, timestamp: Date.now() - 20000 },

  // Nested reply (A reply to a reply)
  { id: 5, userId: 'userD', content: 'A reply to comment #3 (a nested reply).', parentId: 3, timestamp: Date.now() - 10000 },
];

2. The Reusable Comment Form

The form component is simple and “dumb”—it only handles collecting the input text and passing it up to the parent component via the onSubmit prop.

CommentForm.jsx

import React, { useState } from 'react';

const CommentForm = ({ parentId = null, onSubmit }) => {
  const [content, setContent] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (content.trim() === '') return;

    // Pass the content back to the parent component
    onSubmit(content);
    setContent('');
  };

  return (
    <form onSubmit={handleSubmit} style={{ margin: '15px 0', padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }}>
      <h4>{parentId ? `Reply to Comment ${parentId}` : 'Post a New Comment'}</h4>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Write your comment..."
        rows="3"
        style={{ width: '100%', resize: 'vertical', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
      />
      <button 
        type="submit"
        style={{ padding: '8px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
      >
        Submit Comment
      </button>
    </form>
  );
};

export default CommentForm;

3. The Recursive Comment Item

This is the most powerful part of the system. The CommentItem component calls itself (CommentItem) to render its replies, creating the nesting effect.

CommentItem.jsx

import React, { useState } from 'react';
import CommentForm from './CommentForm';

const CommentItem = ({ comment, allComments, onAddReply }) => {
  const [showReplyForm, setShowReplyForm] = useState(false);

  // 1. Find all direct replies to the current comment
  const replies = allComments.filter(c => c.parentId === comment.id);

  const handleReplySubmit = (content) => {
    // Call the centralized function in the main container, passing the parent ID
    onAddReply(comment.id, content);
    setShowReplyForm(false);
  };

  return (
    <div 
      style={{ 
        marginLeft: comment.parentId ? '20px' : '0', 
        borderLeft: comment.parentId ? '2px solid #ccc' : 'none', 
        paddingLeft: comment.parentId ? '10px' : '0', 
        marginTop: '15px',
        paddingBottom: '10px'
      }}
    >
      <div style={{ backgroundColor: comment.parentId ? '#f9f9f9' : '#fff', padding: '10px', borderRadius: '4px' }}>
        <p style={{ margin: '0 0 5px 0' }}>
          <strong>{comment.userId}</strong> 
          <span style={{ color: '#666', fontSize: '0.8em', marginLeft: '10px' }}>
            ({new Date(comment.timestamp).toLocaleTimeString()})
          </span>
        </p>
        <p style={{ margin: '0 0 10px 0' }}>{comment.content}</p>

        {/* Reply button and form toggler */}
        <button 
          onClick={() => setShowReplyForm(!showReplyForm)}
          style={{ background: 'none', border: 'none', color: '#007bff', cursor: 'pointer', padding: 0 }}
        >
          {showReplyForm ? 'Cancel Reply' : 'Reply'}
        </button>
      </div>

      {/* Conditionally render the reply form */}
      {showReplyForm && (
        <CommentForm parentId={comment.id} onSubmit={handleReplySubmit} />
      )}

      {/* 2. Recursively render replies */}
      {replies.length > 0 && (
        <div style={{ marginTop: '10px' }}>
          {replies.map(reply => (
            <CommentItem
              key={reply.id}
              comment={reply}
              allComments={allComments}
              onAddReply={onAddReply}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default CommentItem;

4. The Main Comment Container

The CommentsContainer manages the state, initializes the data, and provides the central function (addComment) for creating both new comments and replies.

CommentsContainer.jsx

import React, { useState } from 'react';
import CommentItem from './CommentItem';
import CommentForm from './CommentForm';
import { initialComments } from './data'; 

// Simple counter for generating temporary front-end IDs
let nextId = initialComments.length + 1;

const CommentsContainer = () => {
  // Use state to hold all comments. In a real app, you'd fetch this with useEffect.
  const [comments, setComments] = useState(initialComments);

  const addComment = (parentId, content) => {
    // --- 1. Create the new comment object ---
    const newComment = {
      id: nextId++,
      userId: 'currentUser', // Placeholder for the authenticated user
      content,
      parentId, // This is the ID of the parent comment, or null for top-level
      timestamp: Date.now(),
    };

    // --- 2. Update the state (simulates a successful API response) ---
    setComments(prevComments => [...prevComments, newComment]);

    // **NOTE ON PRODUCTION:**
    // In a real application, this is where you would call your API (e.g., fetch('/api/comments', { method: 'POST', body: JSON.stringify(newComment) })).
    // You would typically update the state *after* the server confirms the comment was saved.
  };

  // Filter and sort to show only top-level comments for the initial list
  const topLevelComments = comments
    .filter(c => c.parentId === null)
    .sort((a, b) => b.timestamp - a.timestamp); // Sort by newest first

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '30px', fontFamily: 'Inter, sans-serif', backgroundColor: '#f4f4f9', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)' }}>
      <h1 style={{ borderBottom: '2px solid #ddd', paddingBottom: '10px', marginBottom: '20px', color: '#333' }}>
        Article Comments ({comments.length})
      </h1>
      
      {/* Main Comment Form (parentId: null) */}
      <CommentForm 
        onSubmit={(content) => addComment(null, content)} 
        parentId={null} 
      />

      {/* Render the top-level comments, which recursively render their children */}
      <div style={{ marginTop: '30px' }}>
        {topLevelComments.map(comment => (
          <CommentItem
            key={comment.id}
            comment={comment}
            allComments={comments} // Pass the entire list for lookups
            onAddReply={addComment} // Pass the centralized function
          />
        ))}

        {topLevelComments.length === 0 && <p style={{ textAlign: 'center', color: '#666' }}>Be the first to share your thoughts!</p>}
      </div>
    </div>
  );
};

export default CommentsContainer;

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments }

Building fast, interactive web applications is the goal of every developer, and a dynamic search bar that filters a list in real-time is a classic example of this.

It showcases core React principles: state management, event handling, and controlled components.

Let’s dive into the code for a simple but powerful React component, FilterableList, and understand how it brings data to life.


🏗️ The Component Structure

 

Our component is a standard functional component that leverages the useState Hook to manage the data that changes based on user input.

import React, { useState } from 'react';
import './FilterableList.css';

const initialItems = [
  'Apple',
  'Banana',
  'Cherry',
  'Date',
  'Elderberry',
  // ... more fruits
];

function FilterableList() {
  // ... logic and JSX ...
}

The Unchanging Truth: initialItems

 

The initialItems array is the source of truth. This constant array holds the complete, original list of data. It is crucial because we always want to filter from this original list, ensuring that when the user deletes a search query, the full list reappears.


State Management with useState

 

For this component to be dynamic, we need to track two essential pieces of information that change over time:

  1. searchTerm: What the user has currently typed into the input box.

  2. filteredItems: The subset of initialItems that matches the current search query.

  const [searchTerm, setSearchTerm] = useState('');
  const [filteredItems, setFilteredItems] = useState(initialItems);
  • searchTerm starts as an empty string ('').

  • filteredItems starts with the entire initialItems array, so the full list is visible when the component first loads.


🔄 The Engine: handleSearchChange

 

This function is the heart of the filtering logic. It executes every time the user types a character.

  const handleSearchChange = (event) => {
    const query = event.target.value.toLowerCase();

    // 1. Update the Input's Value (State 1)
    setSearchTerm(query);

    // 2. Perform the Filtering Calculation
    const newFilteredItems = initialItems.filter((item) =>
      item.toLowerCase().includes(query)
    );

    // 3. Update the Displayed List (State 2)
    setFilteredItems(newFilteredItems);
  };

Step-by-Step Breakdown:

 

  1. Capture and Standardize Query:

    • const query = event.target.value.toLowerCase();

    • We grab the input text (event.target.value) and immediately convert it to lowercase. This ensures case-insensitive searching—a huge win for user experience.

  2. Update the Search Term State:

    • setSearchTerm(query);

    • This updates the searchTerm state, which is linked to the input’s value prop. This step makes the input a controlled component and triggers React to re-render the component.

  3. Filter the List (The Magic):

    • initialItems.filter(...) creates a new array (newFilteredItems) containing only the items where the item’s lowercase name includes the query string. This is a non-destructive operation, leaving initialItems untouched.

  4. Update the Filtered List State:

    • setFilteredItems(newFilteredItems);

    • This is the second critical state update. By passing the newly calculated array, we tell React to update the filteredItems state. This also triggers a re-render where the JSX will read this new, shorter array.


🖼️ The Display: JSX and Conditional Rendering

 

The return statement is where the state meets the screen.

  return (
    <div className="filterable-list-container">
      {/* ... Input Element ... */}
      <ul className="item-list">
        {filteredItems.length > 0 ? (
          filteredItems.map((item, index) => (
            <li key={index} className="list-item">{item}</li>
          ))
        ) : (
          <li className="no-results">No results found.</li>
        )}
      </ul>
    </div>
  );

The Power of Data Binding:

 

  • Input Control: The <input> has the value={searchTerm} prop. Because searchTerm is state, the input is entirely controlled by React. The onChange prop ensures the state is updated on every keystroke.

  • List Rendering: The <ul> element uses a ternary operator (? :) for conditional rendering.

    • If filteredItems is not empty: We use the map() method on the state variable filteredItems to dynamically generate an <li> element for every match.

    • If filteredItems is empty: We display a simple message: “No results found.”

This entire system beautifully demonstrates the React Data Flow: User action updates state, state change triggers re-render, and the component’s JSX reflects the new state, updating the UI.

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments }