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 |