Most applications need to display lists of data - products in a store, messages in a chat, users in a directory, or items in a todo list. In React, you'll use JavaScript's map() method to transform arrays of data into arrays of JSX elements. But there's a critical concept you need to understand: keys. In this lesson, you'll learn how to render lists properly and why keys are essential for React's performance and correctness.
Rendering Basic Lists
The most common way to render a list in React is using the map() array method. It transforms each item in an array into a JSX element:
Simple List Example
function FruitList() {
const fruits = ['Apple', 'Banana', 'Orange', 'Mango']
return (
<ul>
{fruits.map((fruit) => (
<li>{fruit}</li>
))}
</ul>
)
}
// Renders:
// • Apple
// • Banana
// • Orange
// • MangoHowever, if you try this code, you'll see a warning in the console: "Warning: Each child in a list should have a unique "key" prop."
Adding Keys
To fix the warning, add a key prop to each element:
function FruitList() {
const fruits = ['Apple', 'Banana', 'Orange', 'Mango']
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
)
}
// No more warning!📖 Quick Reminder: map()
array.map(callback) creates a new array by calling the callback function on each element:[1, 2, 3].map(x => x * 2) → [2, 4, 6]
In React, we use it to transform data into JSX elements.
What Are Keys?
Keys are special attributes you add to elements in a list. They help React identify which items have changed, been added, or been removed.
Why Keys Matter
Imagine you have a list of tasks and you add a new task at the beginning. Without keys, React would think:
- The first item changed to the new task
- The second item changed to what was the first task
- The third item changed to what was the second task
- And so on...
This means React would re-render every item in the list, even though only one new item was actually added!
With proper keys, React knows: "Oh, there's a new item with key 123, and all the old items are still there with the same keys. I only need to render the new one!"
Keys Help React:
- Identify which elements changed
- Preserve component state between re-renders
- Improve performance by avoiding unnecessary re-renders
- Maintain focus and selection in forms
- Trigger proper animations
Choosing Good Keys
Not all keys are created equal. Let's explore the right and wrong ways to choose keys:
✅ Best: Stable Unique IDs
The best keys are unique, stable IDs that come from your data:
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
)
}
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
]IDs from databases are perfect because they:
- Are unique for each item
- Don't change when the list is reordered
- Stay the same across renders
⚠️ Acceptable: Generated Stable IDs
If your data doesn't have IDs, you can generate them - but do it once when you create the data, not during render:
// ✅ Good - Generate IDs once when creating data
const todos = [
{ id: crypto.randomUUID(), text: 'Buy groceries' },
{ id: crypto.randomUUID(), text: 'Walk the dog' },
{ id: crypto.randomUUID(), text: 'Write code' }
]
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
// ❌ Bad - Generating during render
function BadTodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={Math.random()}> {/* Never do this! */}
{todo.text}
</li>
))}
</ul>
)
}⚠️ Last Resort: Index as Key
Using the array index as a key works, but it's not ideal:
function SimpleList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)
}Index keys are only safe when:
- The list is static (never reordered or filtered)
- Items have no IDs
- Items are never added/removed from the middle or beginning
Why Index Keys Can Be Problematic
If you reorder, filter, or insert items in the middle of a list, the index of each item changes. This can cause:
• Component state to be attached to the wrong items
• Forms to lose focus or show wrong values
• Animations to behave incorrectly
• Performance issues from unnecessary re-renders
❌ Never: Random Values
// ❌ NEVER DO THIS
{items.map((item) => (
<li key={Math.random()}> {/* Changes every render! */}
{item}
</li>
))}
// ❌ NEVER DO THIS EITHER
{items.map((item) => (
<li key={Date.now()}> {/* Not unique! */}
{item}
</li>
))}These generate new keys on every render, causing React to destroy and recreate elements unnecessarily. This breaks state, causes poor performance, and can introduce bugs.
Rendering Complex List Items
Lists often contain more than just text. Here's how to render complex structures:
Objects with Multiple Properties
function ProductList({ products }) {
return (
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">${product.price}</p>
<button>Add to Cart</button>
</div>
))}
</div>
)
}
const products = [
{
id: 'prod-1',
name: 'Laptop',
price: 999.99,
image: '/laptop.jpg',
description: 'Powerful laptop for work'
},
{
id: 'prod-2',
name: 'Mouse',
price: 29.99,
image: '/mouse.jpg',
description: 'Wireless ergonomic mouse'
}
]Extracting List Items to Components
For complex items, it's cleaner to extract them into separate components:
// Separate component for list item
function ProductCard({ product }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">${product.price}</p>
<button>Add to Cart</button>
</div>
)
}
// Main list component
function ProductList({ products }) {
return (
<div className="product-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}🔑 Where to Put the Key
The key always goes on the top-level element in the map callback, not on child elements:<ProductCard key={id} /> ✅ Correct
Inside ProductCard: <div key={id}> ❌ Wrong
💡 Tip: Edit the code above and click "Run" to see your changes
Nested Lists
Sometimes you need to render lists within lists. Each level needs its own keys:
function CategoryList({ categories }) {
return (
<div>
{categories.map((category) => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
)
}
const categories = [
{
id: 'cat-1',
name: 'Fruits',
items: [
{ id: 'fruit-1', name: 'Apple' },
{ id: 'fruit-2', name: 'Banana' }
]
},
{
id: 'cat-2',
name: 'Vegetables',
items: [
{ id: 'veg-1', name: 'Carrot' },
{ id: 'veg-2', name: 'Broccoli' }
]
}
]Notice how each level of the list has its own key prop. The outer map needs keys for categories, and the inner map needs keys for items.
Keys and Component State
Here's a crucial concept: React uses keys to match components between renders. When a key changes, React destroys the old component and creates a new one, resetting its state.
Example: Wrong Keys Break State
function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task, index) => (
// ❌ Using index as key
<TaskItem key={index} task={task} />
))}
</ul>
)
}
function TaskItem({ task }) {
const [isEditing, setIsEditing] = React.useState(false)
return (
<li>
{isEditing ? (
<input defaultValue={task.text} />
) : (
<span>{task.text}</span>
)}
<button onClick={() => setIsEditing(!isEditing)}>
Edit
</button>
</li>
)
}
// Problem: If you reorder tasks, the editing state
// stays with the position, not the task!Solution: Proper Keys Preserve State
function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task) => (
// ✅ Using unique ID as key
<TaskItem key={task.id} task={task} />
))}
</ul>
)
}
// Now the editing state follows the task,
// not the position in the list!Common List Patterns
Filtering Lists
function TodoList({ todos, filter }) {
const filteredTodos = todos.filter(todo => {
if (filter === 'all') return true
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
return true
})
return (
<ul>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
)
}Sorting Lists
function UserList({ users, sortBy }) {
const sortedUsers = [...users].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
if (sortBy === 'age') {
return a.age - b.age
}
return 0
})
return (
<ul>
{sortedUsers.map((user) => (
<UserCard key={user.id} user={user} />
))}
</ul>
)
}
// Note: Spread operator [...users] creates a copy
// because .sort() modifies the original arrayTransforming Data Before Rendering
function PriceList({ products }) {
return (
<ul>
{products.map((product) => {
const discountedPrice = product.price * 0.9
const savings = product.price - discountedPrice
return (
<li key={product.id}>
<h3>{product.name}</h3>
<p>
<span className="original">${product.price}</span>
<span className="discounted">${discountedPrice.toFixed(2)}</span>
<span className="savings">Save ${savings.toFixed(2)}</span>
</p>
</li>
)
})}
</ul>
)
}Adding Index to Items
function RankingList({ players }) {
return (
<ol>
{players.map((player, index) => (
<li key={player.id}>
#{index + 1} - {player.name} ({player.score} points)
</li>
))}
</ol>
)
}
// Note: We use index for display, but player.id for the key!Empty List Handling
function MessageList({ messages }) {
if (messages.length === 0) {
return (
<div className="empty-state">
<p>No messages yet</p>
<p>Start a conversation!</p>
</div>
)
}
return (
<ul>
{messages.map((message) => (
<MessageItem key={message.id} message={message} />
))}
</ul>
)
}Performance Considerations
When working with large lists (thousands of items), consider these optimizations:
1. Use Key-Based Reconciliation
Proper keys are the first line of performance optimization. They help React avoid re-rendering unchanged items.
2. Avoid Inline Functions
// ❌ Creates new function on every render
function ProductList({ products }) {
return (
<div>
{products.map((product) => (
<div key={product.id}>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
)
}
// ✅ Extract to a component
function ProductItem({ product, onAddToCart }) {
return (
<div>
<button onClick={() => onAddToCart(product)}>
Add to Cart
</button>
</div>
)
}
function ProductList({ products, onAddToCart }) {
return (
<div>
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
)
}3. Memoize List Items
For very large lists with expensive render logic, you can use React.memo() (we'll cover this in detail later):
const ProductItem = React.memo(function ProductItem({ product }) {
console.log('Rendering product:', product.name)
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
)
})
// Now ProductItem only re-renders when product actually changes4. Virtualization for Huge Lists
For lists with thousands of items, consider using virtualization libraries like react-window or react-virtual that only render visible items.
Common Mistakes to Avoid
1. Forgetting Keys
// ❌ Missing keys
{items.map((item) => (
<div>{item.name}</div>
))}
// ✅ Add keys
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}2. Using Non-Unique Keys
// ❌ Duplicate keys if multiple items have same name
{items.map((item) => (
<div key={item.name}>{item.name}</div>
))}
// ✅ Use guaranteed unique ID
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}3. Putting Key on Wrong Element
// ❌ Key inside the component
function UserCard({ user }) {
return <div key={user.id}>{user.name}</div>
}
{users.map((user) => <UserCard user={user} />)}
// ✅ Key on the mapped element
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}4. Modifying Array While Mapping
// ❌ Never modify the array while mapping
{items.map((item, index) => {
items[index].viewed = true // Don't do this!
return <div key={item.id}>{item.name}</div>
})}
// ✅ Transform data before mapping
const viewedItems = items.map(item => ({ ...item, viewed: true }))
{viewedItems.map((item) => (
<div key={item.id}>{item.name}</div>
))}5. Using Index When Items Can Reorder
// ❌ Using index when list can be reordered/filtered
function SortableList({ items }) {
const [sortedItems, setSortedItems] = useState(items)
return (
<ul>
{sortedItems.map((item, index) => (
<li key={index}>{item.name}</li> // Bad!
))}
</ul>
)
}
// ✅ Use stable IDs
function SortableList({ items }) {
const [sortedItems, setSortedItems] = useState(items)
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id}>{item.name}</li> // Good!
))}
</ul>
)
}Interactive Example: Task Manager
💡 Tip: Edit the code above and click "Run" to see your changes
Key Takeaways
- Use
map()to transform arrays of data into JSX elements - Every element in a list needs a unique
keyprop - Keys help React identify which items changed, were added, or removed
- Best keys are stable, unique IDs from your data
- Avoid using array index as key when items can reorder
- Never use random values or
Date.now()as keys - Put the key on the top-level element returned from
map() - Keys must be unique among siblings, not globally
- When a key changes, React destroys and recreates the component
- Extract complex list items into separate components
- Handle empty lists gracefully with conditional rendering
- For huge lists, consider virtualization libraries
What's Next?
You now know how to render lists efficiently and understand why keys are crucial for React's performance and correctness. You can display arrays of data, filter and sort them, and handle complex list structures.
But your lists might still look a bit plain. In the next lesson, we'll dive deep into styling in JSX. You'll learn multiple ways to style React components - inline styles, CSS classes, CSS modules, and more. Get ready to make your components look amazing! 🎨