Lists and Keys
Learn how to render lists of data efficiently and understand why React needs keys for optimal performance.
Learning Objectives
- Understand why lists are essential in web apps
- Learn to render arrays of data with map()
- Master the key prop and why it matters
- Build interactive lists with add/remove functionality
- Handle complex list operations efficiently
- Avoid common list rendering mistakes
Lists and Keys
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.
Why Are Lists So Important?
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:
- Add new todos?
- Delete completed todos?
- Mark todos as done?
- Have 100 todos instead of 3?
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.
From Static to Dynamic: Your First List
Let's transform a static list into a dynamic one step by step.
Step 1: Start with an Array
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> ); }
Step 2: Use the map() Function
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- For each fruit, it creates a
<li>
element - The
{}
tells React to treat this as JavaScript - React displays all the
<li>
elements
Step 3: Add the Key Prop (Important!)
If 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!
Understanding the map() Function
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():
- It doesn't change the original array
- It returns a new array
- You provide a function that transforms each item
- In React, we transform data into JSX elements
Let's Build a Real Todo List
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?
- ✅ Uses proper keys (
todo.id
) - ✅ Handles empty state
- ✅ Shows interactive list operations
- ✅ Demonstrates real-world patterns
The Mystery of Keys: Why Does React Need Them?
You might wonder: "Why does React care about keys?" Great question! Let me explain with a story.
The Problem Without Keys
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:
- Re-render everything unnecessarily
- Lose component state
- Have poor performance
The Solution: Unique Keys
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!
Rules for Good Keys
Rule 1: Keys Must Be Unique
// ❌ Bad - duplicate keys {items.map(item => <li key="same-key">{item}</li>)} // ✅ Good - unique keys {items.map(item => <li key={item.id}>{item}</li>)}
Rule 2: Keys Should Be Stable
// ❌ 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>)}
Rule 3: Avoid Array Index When Order Can Change
// ❌ 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).
Different Types of Lists
Simple String Arrays
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> ); }
Object Arrays (Most Common)
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> ); }
Nested Lists
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> ); }
Advanced List Operations
Filtering Lists
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> ); }
Sorting Lists
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> ); }
Practice Exercise: Build a Contact List
Try building this contact list with the following features:
Requirements:
- Display a list of contacts with name, email, and phone
- Add new contacts with a form
- Delete contacts
- Search/filter contacts by name
- Show total count
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> ); }
Common List Mistakes and Solutions
Mistake 1: Missing Keys
// ❌ React will warn about missing keys {items.map(item => <li>{item}</li>)} // ✅ Always include keys {items.map(item => <li key={item.id}>{item}</li>)}
Mistake 2: Mutating State Arrays
// ❌ 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! };
Mistake 3: Using Index as Key for Dynamic Lists
// ❌ 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> ))}
Mistake 4: Not Handling Empty Lists
// ❌ 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> )}
Performance Tips for Lists
Tip 1: Use Unique, Stable Keys
- Helps React optimize re-renders
- Prevents component state loss
- Improves animation performance
Tip 2: Keep List Items Simple
- Extract complex logic to separate functions
- Avoid creating objects/functions in render
- Use React.memo for expensive list items
Tip 3: Handle Large Lists Carefully
- Consider virtualization for 1000+ items
- Implement pagination when appropriate
- Use search/filtering to reduce displayed items
What We've Learned
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
Quick Recap Quiz
Test your knowledge:
- What JavaScript method do you use to render arrays in React?
- Why are keys important in React lists?
- When is it okay to use array index as a key?
- How do you add an item to a state array without mutating it?
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]
What's Next?
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!