Lists and Keys — Rendering Dynamic Collections

Rendering dynamic lists is one of the most common React operations — post feeds, comment threads, tag clouds, user lists. React renders arrays by mapping each item to a JSX element. The key prop on each element is React’s mechanism for tracking items across re-renders: when the list changes (items added, removed, or reordered), React uses keys to determine which DOM nodes to add, remove, or move efficiently. Using incorrect keys — or none at all — causes UI bugs ranging from incorrect animations to inputs showing the wrong values after reorder.

Rendering Lists with map()

// ── Basic list rendering ───────────────────────────────────────────────────────
function TagCloud({ tags }) {
    return (
        <div className="flex flex-wrap gap-2">
            {tags.map((tag) => (
                <a
                    key={tag.id}                 // ← key: stable unique ID from data
                    href={`/tags/${tag.slug}`}
                    className="text-sm bg-gray-100 px-3 py-1 rounded-full hover:bg-gray-200"
                >
                    {tag.name}
                    <span className="ml-1 text-gray-400">{tag.post_count}</span>
                </a>
            ))}
        </div>
    );
}

// ── Filtering and sorting before render ───────────────────────────────────────
function PostList({ posts, filterStatus = "published", sortBy = "date" }) {
    const filtered = posts
        .filter((p) => p.status === filterStatus)
        .sort((a, b) => {
            if (sortBy === "date")  return new Date(b.created_at) - new Date(a.created_at);
            if (sortBy === "views") return b.view_count - a.view_count;
            return a.title.localeCompare(b.title);
        });

    return (
        <div className="grid gap-4">
            {filtered.map((post) => (
                <PostCard key={post.id} post={post} />
            ))}
        </div>
    );
}
Note: The key prop must be unique among siblings in a list, but does not need to be globally unique. The keys in a PostList (post IDs) and the keys in a CommentList on the same page (comment IDs) can overlap without issue — React tracks keys per parent element, not globally. What matters is that keys are unique within a single map() call and are stable across renders (the same item should always have the same key).
Tip: When you have a list with no natural ID (like a list of strings or temporary items), create stable IDs rather than using array indices. For a list of user-entered tags before saving, generate a unique ID at creation time: const newTag = { id: crypto.randomUUID(), name: inputValue }. The ID persists for the lifetime of that tag in local state, giving React a stable key. Never use Math.random() as a key — a new random value on every render defeats the purpose of keys entirely.
Warning: Never mutate state arrays directly. posts.push(newPost) and posts[0].title = "changed" do not trigger re-renders because React uses reference equality to detect state changes — the array reference did not change. Always create a new array: setPosts([...posts, newPost]) for adding, setPosts(posts.filter(p => p.id !== id)) for removing, and setPosts(posts.map(p => p.id === id ? {...p, title: "new"} : p)) for updating one item.

Empty States

function PostList({ posts = [], isLoading = false }) {
    // Empty state: no posts
    if (!isLoading && posts.length === 0) {
        return (
            <div className="text-center py-16">
                <svg className="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1}
                          stroke="currentColor" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5..." />
                </svg>
                <h3 className="mt-2 text-sm font-medium text-gray-900">No posts</h3>
                <p className="mt-1 text-sm text-gray-500">Get started by writing a new post.</p>
                <a href="/posts/new" className="mt-4 inline-block px-4 py-2 bg-blue-600 text-white rounded">
                    New Post
                </a>
            </div>
        );
    }

    return (
        <div className="grid gap-4">
            {posts.map((post) => (
                <PostCard key={post.id} post={post} />
            ))}
        </div>
    );
}

Immutable Array Patterns

// These patterns update state arrays without mutating the original

// ── Add item ──────────────────────────────────────────────────────────────────
setPosts((prev) => [newPost, ...prev]);            // prepend
setPosts((prev) => [...prev, newPost]);            // append

// ── Remove item ───────────────────────────────────────────────────────────────
setPosts((prev) => prev.filter((p) => p.id !== deletedId));

// ── Update one item ───────────────────────────────────────────────────────────
setPosts((prev) =>
    prev.map((p) => (p.id === updatedPost.id ? { ...p, ...updatedPost } : p))
);

// ── Sort (create new sorted array) ───────────────────────────────────────────
// ❌ Wrong — sort() mutates the original array!
// posts.sort((a, b) => a.title.localeCompare(b.title))

// ✓ Correct — spread to copy first, then sort
const sorted = [...posts].sort((a, b) => a.title.localeCompare(b.title));

Common Mistakes

Mistake 1 — Array index as key

❌ Wrong — key 0 always belongs to whichever item is first, even after reorder:

{posts.map((p, i) => <PostCard key={i} post={p} />)}
// Deleting post at index 2: indices shift → keys change → React re-renders everything!

✅ Correct — use stable item ID:

{posts.map((p) => <PostCard key={p.id} post={p} />)}   // ✓

Mistake 2 — Mutating state arrays

❌ Wrong — no re-render triggered:

posts.push(newPost);   // mutates in place — React doesn't see the change
setPosts(posts);       // same reference → React skips re-render!

✅ Correct:

setPosts([...posts, newPost]);   // ✓ new array reference triggers re-render

Quick Reference

Operation Immutable Pattern
Render list items.map((item) => <C key={item.id} item={item} />)
Add item setItems(prev => [...prev, newItem])
Remove item setItems(prev => prev.filter(i => i.id !== id))
Update item setItems(prev => prev.map(i => i.id === id ? {...i, ...changes} : i))
Sort (safe) [...items].sort((a, b) => ...)
Filter + sort items.filter(...).sort(...) ← filter returns new array

🧠 Test Yourself

You have a list of posts with text inputs for editing. Each PostCard has an input. You use array index as key. A user deletes post at index 1 (middle of 3). What happens to the input values?