Rendering Elements and Lists in React

Rendering is how React turns your component tree into the user interface visible in the browser. Understanding how React renders elements โ€” when a component re-renders, how conditional rendering controls what is shown, and how the map method turns data arrays into lists of elements โ€” are core skills you will use in every component you write in the MERN Blog client. This lesson covers all three patterns with practical examples from the blog domain.

How React Renders

Initial render:
  1. React calls your component function
  2. The function returns JSX
  3. React creates DOM nodes from the JSX
  4. React inserts them into the page at the <div id="root"> mount point

Re-render (after state or prop change):
  1. React calls your component function again with the new state/props
  2. The function returns new JSX
  3. React diffs the new virtual DOM against the previous one
  4. React applies only the changed DOM nodes โ€” not a full page refresh

When does a component re-render?
  โœ“ Its own state changes (useState setter is called)
  โœ“ A prop passed to it changes
  โœ“ Its parent re-renders (unless memoised with React.memo)
  โœ— A variable inside the component changes (not tracked by React)
Note: React’s rendering model is pure functions. A component function should take props and state as inputs and return JSX as output, with no side effects during rendering. Side effects (API calls, DOM mutations, subscriptions) belong in useEffect โ€” not in the function body. When a component re-renders, React may call it multiple times (especially in StrictMode) to detect side effects โ€” pure rendering functions are safe to call multiple times.
Tip: When conditionally rendering a large component tree, wrap the condition in a dedicated component. Instead of embedding a 50-line conditional block inside your page component, extract it to <PostListSection posts={posts} isLoading={loading} error={error} /> and put the conditional logic there. This keeps your page component readable and makes each piece independently testable.
Warning: The key prop is required on every element produced inside a .map(). Without it, React cannot efficiently reconcile the list when items are added, removed, or reordered โ€” it will either produce a console warning or, worse, silently update the wrong element. The key must be stable (same value across re-renders) and unique among siblings. Always use a database ID like post._id โ€” never use the array index as a key for dynamic lists.

Conditional Rendering โ€” Three Patterns

// โ”€โ”€ Pattern 1: if/else before the return โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function PostPage({ postId }) {
  const [post,    setPost]    = useState(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  // ... useEffect to load post ...

  // Use if/else for complex multi-branch conditions BEFORE the return
  if (loading) return <div className="spinner">Loading...</div>;
  if (error)   return <div className="error">Error: {error}</div>;
  if (!post)   return <div className="not-found">Post not found</div>;

  // Only reaches here if post is loaded and valid
  return <article><h1>{post.title}</h1></article>;
}
// โ”€โ”€ Pattern 2: Ternary operator โ€” inside JSX for two-branch conditions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function PostCard({ post }) {
  return (
    <article>
      <h2>{post.title}</h2>
      {/* Ternary: show one thing or another */}
      {post.published
        ? <span className="badge badge--published">Published</span>
        : <span className="badge badge--draft">Draft</span>
      }
      {/* Nested ternary โ€” keep shallow, max 1โ€“2 levels */}
      <p>{post.viewCount > 1000 ? 'Trending' : post.viewCount > 100 ? 'Popular' : 'New'}</p>
    </article>
  );
}
// โ”€โ”€ Pattern 3: Logical AND โ€” show or show nothing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function PostCard({ post, isOwner }) {
  return (
    <article>
      <h2>{post.title}</h2>

      {/* Render only if condition is truthy */}
      {post.featured && <span className="badge badge--featured">โญ Featured</span>}

      {/* Only show edit/delete to the post owner */}
      {isOwner && (
        <div className="post-actions">
          <button>Edit</button>
          <button>Delete</button>
        </div>
      )}
    </article>
  );
}

Rendering Lists with map()

// โ”€โ”€ Basic list rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function TagList({ tags }) {
  return (
    <ul className="tag-list">
      {tags.map(tag => (
        <li key={tag} className="tag">
          #{tag}
        </li>
      ))}
    </ul>
  );
}

// โ”€โ”€ List of objects with components โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function PostList({ posts }) {
  if (posts.length === 0) {
    return <p className="empty-state">No posts found.</p>;
  }

  return (
    <div className="post-list">
      {posts.map(post => (
        // key uses post._id from MongoDB โ€” stable and unique
        <PostCard key={post._id} post={post} />
      ))}
    </div>
  );
}

// โ”€โ”€ Filtered list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function PublishedPosts({ posts }) {
  const published = posts.filter(p => p.published);

  return (
    <ul>
      {published.map(post => (
        <li key={post._id}>{post.title}</li>
      ))}
    </ul>
  );
}

// โ”€โ”€ Sorted list with conditional class โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function SortedPostList({ posts }) {
  const sorted = [...posts].sort((a, b) => b.viewCount - a.viewCount);

  return (
    <ol className="ranked-list">
      {sorted.map((post, index) => (
        <li
          key={post._id}
          className={index === 0 ? 'post-item post-item--top' : 'post-item'}
        >
          #{index + 1} โ€” {post.title} ({post.viewCount} views)
        </li>
      ))}
    </ol>
  );
}

The Key Prop โ€” Why It Matters

Without key โ€” React cannot track which list item is which:
  Posts: [A, B, C]
  After delete B: [A, C]
  React sees: "list went from 3 to 2 items โ€” update item 2 to C's data,
               remove item 3" โ†’ potentially re-renders the WRONG component

With key={post._id} โ€” React tracks identity:
  Posts: [{ _id: 1, title: A }, { _id: 2, title: B }, { _id: 3, title: C }]
  After delete _id:2: [{ _id: 1, title: A }, { _id: 3, title: C }]
  React sees: "item with key=2 was removed โ€” keep 1 and 3 as-is"
  โ†’ Correct, efficient DOM update

Why NOT to use array index as key:
  posts.map((post, index) => <PostCard key={index} ... />)
  If posts are sorted or filtered, the indices change โ†’ React thinks
  different items are the same item โ†’ state bugs and stale UI

Common Mistakes

Mistake 1 โ€” Using array index as key for dynamic lists

โŒ Wrong โ€” index-based key causes bugs when items are reordered or filtered:

{posts.map((post, index) => <PostCard key={index} post={post} />)}
// If posts are sorted or a post is deleted โ€” keys shift โ†’ React updates wrong components

โœ… Correct โ€” use the stable unique ID from MongoDB:

{posts.map(post => <PostCard key={post._id} post={post} />)}  // โœ“

Mistake 2 โ€” Using 0 as a falsy value in logical AND rendering

โŒ Wrong โ€” 0 is falsy and renders as “0” in JSX, not as nothing:

{posts.length && <PostList posts={posts} />}
// If posts.length === 0 โ†’ renders "0" in the DOM instead of nothing!

โœ… Correct โ€” use a boolean expression:

{posts.length > 0 && <PostList posts={posts} />}    // โœ“
// or:
{!!posts.length && <PostList posts={posts} />}     // โœ“

Mistake 3 โ€” Forgetting to handle the empty list case

โŒ Wrong โ€” rendering nothing when the list is empty, leaving the user confused:

{posts.map(post => <PostCard key={post._id} post={post} />)}
// When posts = [] โ†’ renders nothing โ€” user sees a blank page with no explanation

โœ… Correct โ€” always handle the empty state:

{posts.length === 0
  ? <p className="empty">No posts yet. Be the first to write one!</p>
  : posts.map(post => <PostCard key={post._id} post={post} />)
}

Quick Reference

Pattern When to Use Code
if before return Loading/error/not-found guards if (loading) return <Spinner />
Ternary Show A or show B {cond ? <A /> : <B />}
Logical AND Show or show nothing {isOwner && <EditButton />}
Array.map() Render a list from data {posts.map(p => <PostCard key={p._id} post={p} />)}
Empty state No items in list {items.length === 0 && <p>No items</p>}
Stable key List item identity key={item._id} (from database)

🧠 Test Yourself

You render a list of posts with {posts.map((post, i) => <PostCard key={i} post={post} />)}. The user deletes the second post. What problem can this cause?