Back to Tutorial
State and Events
Hands-on Practice
30 min

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?

  1. fruits.map() goes through each fruit in the array
  2. For each fruit, it creates a <li> element
  3. The {} tells React to treat this as JavaScript
  4. 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:

  1. Display a list of contacts with name, email, and phone
  2. Add new contacts with a form
  3. Delete contacts
  4. Search/filter contacts by name
  5. 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:

  1. What JavaScript method do you use to render arrays in React?
  2. Why are keys important in React lists?
  3. When is it okay to use array index as a key?
  4. 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!