Learn how to render lists of data efficiently and understand why React needs keys for optimal performance.
Think about any app you use daily - Instagram shows a list of posts, Spotify shows a list of songs, Amazon shows a list of products. Lists are everywhere! Almost every real application needs to display collections of data dynamically.
In this lesson, you'll learn how to turn boring static content into dynamic, interactive lists that can grow, shrink, and change based on user actions.
Imagine you're building a todo app. You could hardcode each todo item:
function TodoApp() {
return (
<ul>
<li>Buy groceries</li>
<li>Walk the dog</li>
<li>Learn React</li>
</ul>
);
}
But what happens when users want to:
You'd need to manually update your code every time! That's not practical.
Lists solve this problem by letting you display data dynamically from arrays.
Let's transform a static list into a dynamic one step by step.
Instead of hardcoding items, let's use an array:
function FruitList() {
const fruits = ["๐ Apple", "๐ Banana", "๐ Orange"];
return (
<div>
<h2>My Favorite Fruits</h2>
{/* How do we display this array? */}
</div>
);
}
The map() function is your new best friend! It transforms each item in an array into JSX:
function FruitList() {
const fruits = ["๐ Apple", "๐ Banana", "๐ Orange"];
return (
<div>
<h2>My Favorite Fruits</h2>
<ul>
{fruits.map((fruit) => (
<li>{fruit}</li>
))}
</ul>
</div>
);
}
What's happening here?
fruits.map() goes through each fruit in the array<li> element{} tells React to treat this as JavaScript<li> elementsIf you try the code above, React will show a warning in the console. Here's the fixed version:
function FruitList() {
const fruits = ["๐ Apple", "๐ Banana", "๐ Orange"];
return (
<div>
<h2>My Favorite Fruits</h2>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
</div>
);
}
We'll learn more about keys in a moment, but for now, just remember: every list item needs a key prop!
Let's break down map() because it's crucial for lists:
// Think of map() like this:
const numbers = [1, 2, 3];
// map() creates a NEW array by transforming each item
const doubledNumbers = numbers.map((number) => number * 2);
// Result: [2, 4, 6]
// In React, we transform data into JSX
const numberElements = numbers.map((number) => <p>{number}</p>);
// Result: [<p>1</p>, <p>2</p>, <p>3</p>]
Key points about map():
Now let's create something more practical - a todo list where users can add and remove items:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Build a project", completed: false },
{ id: 3, text: "Get a job", completed: false }
]);
const [newTodo, setNewTodo] = useState("");
const addTodo = () => {
if (newTodo.trim()) { // Only add if not empty
const todo = {
id: Date.now(), // Simple ID generation
text: newTodo,
completed: false
};
setTodos([...todos, todo]); // Add to the end
setNewTodo(""); // Clear input
}
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<div style={{ padding: "20px", maxWidth: "400px" }}>
<h2>๐ My Todo List</h2>
{/* Add new todo */}
<div style={{ marginBottom: "20px" }}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo..."
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
style={{ marginRight: "10px", padding: "5px" }}
/>
<button onClick={addTodo}>Add</button>
</div>
{/* Todo list */}
{todos.length === 0 ? (
<p>No todos yet! Add one above.</p>
) : (
<ul style={{ listStyle: "none", padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
padding: "10px",
backgroundColor: todo.completed ? "#d4edda" : "#fff",
border: "1px solid #ddd",
borderRadius: "5px",
marginBottom: "5px"
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: "10px" }}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? "line-through" : "none",
color: todo.completed ? "#6c757d" : "#000"
}}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
style={{
backgroundColor: "#dc3545",
color: "white",
border: "none",
padding: "5px 10px",
borderRadius: "3px",
cursor: "pointer"
}}
>
Delete
</button>
</li>
))}
</ul>
)}
{/* Summary */}
<div style={{ marginTop: "20px", fontSize: "14px", color: "#666" }}>
Total: {todos.length} |
Completed: {todos.filter(t => t.completed).length} |
Remaining: {todos.filter(t => !t.completed).length}
</div>
</div>
);
}
What makes this example great?
todo.id)You might wonder: "Why does React care about keys?" Great question! Let me explain with a story.
Imagine React is like a librarian trying to keep track of books on a shelf:
// Without keys, React sees this:
<ul>
<li>Book 1</li>
<li>Book 2</li>
<li>Book 3</li>
</ul>
// If you add a book at the beginning:
<ul>
<li>NEW BOOK</li> {/* React thinks this was "Book 1" */}
<li>Book 1</li> {/* React thinks this was "Book 2" */}
<li>Book 2</li> {/* React thinks this was "Book 3" */}
<li>Book 3</li> {/* React thinks this is completely new */}
</ul>
React gets confused and might:
With keys, it's like giving each book a barcode:
// With keys, React can track each item precisely:
<ul>
<li key="new">NEW BOOK</li> {/* React: "Oh, this is new!" */}
<li key="book1">Book 1</li> {/* React: "I know this one, just moved!" */}
<li key="book2">Book 2</li> {/* React: "This one moved too!" */}
<li key="book3">Book 3</li> {/* React: "And this one!" */}
</ul>
Now React can efficiently update only what changed!
// โ Bad - duplicate keys
{items.map(item => <li key="same-key">{item}</li>)}
// โ
Good - unique keys
{items.map(item => <li key={item.id}>{item}</li>)}
// โ Bad - keys change every render
{items.map(item => <li key={Math.random()}>{item}</li>)}
// โ
Good - keys stay the same
{items.map(item => <li key={item.id}>{item}</li>)}
// โ Problematic when items can be reordered
{items.map((item, index) => <li key={index}>{item}</li>)}
// โ
Better - use unique property
{items.map(item => <li key={item.id}>{item}</li>)}
When is index okay? When the list never changes order (like a static menu).
function ColorList() {
const colors = ["red", "green", "blue", "yellow"];
return (
<ul>
{colors.map((color, index) => (
<li
key={color} // color is unique, so we can use it
style={{ color: color }}
>
{color}
</li>
))}
</ul>
);
}
function UserList() {
const users = [
{ id: 1, name: "Alice", email: "alice@example.com", role: "Admin" },
{ id: 2, name: "Bob", email: "bob@example.com", role: "User" },
{ id: 3, name: "Charlie", email: "charlie@example.com", role: "Editor" }
];
return (
<div>
{users.map(user => (
<div
key={user.id}
style={{
border: "1px solid #ccc",
padding: "10px",
margin: "10px 0",
borderRadius: "5px"
}}
>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<span
style={{
backgroundColor: user.role === "Admin" ? "#28a745" : "#17a2b8",
color: "white",
padding: "2px 8px",
borderRadius: "12px",
fontSize: "12px"
}}
>
{user.role}
</span>
</div>
))}
</div>
);
}
function CategoryList() {
const categories = [
{
id: 1,
name: "Fruits",
items: ["Apple", "Banana", "Orange"]
},
{
id: 2,
name: "Vegetables",
items: ["Carrot", "Broccoli", "Spinach"]
}
];
return (
<div>
{categories.map(category => (
<div key={category.id} style={{ marginBottom: "20px" }}>
<h3>{category.name}</h3>
<ul>
{category.items.map((item, index) => (
<li key={`\${category.id}-\${index}`}>
{item}
</li>
))}
</ul>
</div>
))}
</div>
);
}
function FilterableList() {
const [filter, setFilter] = useState("");
const [items] = useState([
"Apple", "Banana", "Cherry", "Date", "Elderberry"
]);
const filteredItems = items.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Filter items..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
style={{ marginBottom: "10px", padding: "5px" }}
/>
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
{filteredItems.length === 0 && (
<p>No items match "{filter}"</p>
)}
</div>
);
}
function SortableList() {
const [sortBy, setSortBy] = useState("name");
const [sortOrder, setSortOrder] = useState("asc");
const [people] = useState([
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
{ id: 3, name: "Charlie", age: 35 }
]);
const sortedPeople = [...people].sort((a, b) => {
let comparison = 0;
if (sortBy === "name") {
comparison = a.name.localeCompare(b.name);
} else if (sortBy === "age") {
comparison = a.age - b.age;
}
return sortOrder === "asc" ? comparison : -comparison;
});
return (
<div>
<div style={{ marginBottom: "10px" }}>
<label>
Sort by:
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
style={{ margin: "0 10px" }}
>
<option value="name">Name</option>
<option value="age">Age</option>
</select>
</label>
<label>
Order:
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
style={{ margin: "0 10px" }}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</label>
</div>
<ul>
{sortedPeople.map(person => (
<li key={person.id}>
{person.name} (Age: {person.age})
</li>
))}
</ul>
</div>
);
}
Try building this contact list with the following features:
Requirements:
Starter code:
function ContactList() {
const [contacts, setContacts] = useState([
{ id: 1, name: "John Doe", email: "john@example.com", phone: "555-0101" },
{ id: 2, name: "Jane Smith", email: "jane@example.com", phone: "555-0102" }
]);
const [searchTerm, setSearchTerm] = useState("");
const [newContact, setNewContact] = useState({
name: "",
email: "",
phone: ""
});
// Your code here!
// Implement: addContact, deleteContact, filteredContacts
return (
<div style={{ padding: "20px", maxWidth: "600px" }}>
<h2>๐ Contact List</h2>
{/* Search */}
<input
type="text"
placeholder="Search contacts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: "100%", padding: "10px", marginBottom: "20px" }}
/>
{/* Add Contact Form */}
<div style={{ marginBottom: "20px", padding: "15px", border: "1px solid #ddd" }}>
<h3>Add New Contact</h3>
{/* Add form inputs here */}
</div>
{/* Contact List */}
<div>
<h3>Contacts ({/* show count */})</h3>
{/* Display filtered contacts here */}
</div>
</div>
);
}
// โ React will warn about missing keys
{items.map(item => <li>{item}</li>)}
// โ
Always include keys
{items.map(item => <li key={item.id}>{item}</li>)}
// โ Don't mutate the original array
const addItem = (newItem) => {
items.push(newItem); // Bad!
setItems(items);
};
// โ
Create a new array
const addItem = (newItem) => {
setItems([...items, newItem]); // Good!
};
// โ Problematic when list items can be reordered
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
// โ
Use unique, stable identifiers
{items.map(item => (
<div key={item.id}>{item}</div>
))}
// โ Shows nothing when list is empty
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
// โ
Provide feedback for empty states
{items.length === 0 ? (
<p>No items found</p>
) : (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
Congratulations! You now know how to:
โ
Render arrays of data with map()
โ
Use keys properly for optimal performance
โ
Build interactive lists with add/remove functionality
โ
Filter and sort lists dynamically
โ
Handle empty states and edge cases
โ
Avoid common list rendering pitfalls
Test your knowledge:
Answers: 1) map(), 2) They help React track and optimize list updates, 3) When the list order never changes, 4) Use spread operator: [...items, newItem]
In our next lesson, we'll learn about useEffect - React's powerful hook for handling side effects like API calls, timers, and cleanup. You'll discover how to fetch data for your lists, respond to changes, and manage component lifecycle.
With useEffect, your lists can become truly dynamic by loading data from APIs and updating in real-time!