Lists & Keys
Rendering lists of data is one of the most common tasks in React. Understanding how to do it correctly and efficiently is crucial for building performant applications.Rendering Lists with map()
Use JavaScript’smap() method to transform an array of data into an array of elements.
Copy
function SimpleList() {
const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
}
Rendering Objects
Copy
function UserList() {
const users = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
{ id: 3, name: 'Charlie', email: '[email protected]' }
];
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong> - {user.email}
</li>
))}
</ul>
);
}
Understanding Keys
Keys are special string attributes you need to include when creating lists of elements. They help React identify which items have changed, been added, or been removed.Why Keys Matter
React uses keys during its reconciliation process to efficiently update the DOM:Copy
Without Keys:
┌──────────────────────────────────────────────────────────┐
│ Old List Change New List │
│ ┌─────────┐ ┌─────────┐ │
│ │ Item A │ Insert X │ Item X │ ← Recreated │
│ ├─────────┤ at start ├─────────┤ │
│ │ Item B │ │ Item A │ ← Recreated │
│ ├─────────┤ ├─────────┤ │
│ │ Item C │ │ Item B │ ← Recreated │
│ └─────────┘ ├─────────┤ │
│ │ Item C │ ← Recreated │
│ └─────────┘ │
│ React rebuilds EVERYTHING (O(n) operations) │
└──────────────────────────────────────────────────────────┘
With Keys:
┌──────────────────────────────────────────────────────────┐
│ Old List Change New List │
│ ┌─────────┐ ┌─────────┐ │
│ │ key="a" │ Insert X │ key="x" │ ← NEW │
│ ├─────────┤ at start ├─────────┤ │
│ │ key="b" │ │ key="a" │ ← Moved │
│ ├─────────┤ ├─────────┤ │
│ │ key="c" │ │ key="b" │ ← Moved │
│ └─────────┘ ├─────────┤ │
│ │ key="c" │ ← Moved │
│ └─────────┘ │
│ React only INSERTS the new item (O(1) operation) │
└──────────────────────────────────────────────────────────┘
Key Rules
- Keys must be unique among siblings (not globally unique)
- Keys should be stable (same item = same key across renders)
- Keys should not change (don’t use random values)
What to Use as Keys
Copy
// ✅ BEST: Database IDs
{users.map(user => <UserCard key={user.id} user={user} />)}
// ✅ GOOD: Unique identifiers in data
{products.map(product => <ProductCard key={product.sku} product={product} />)}
// ✅ OK: Generated unique IDs (when adding items)
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: crypto.randomUUID(), text }]);
};
// ⚠️ CAUTION: Index (only for static lists that never reorder)
{staticItems.map((item, index) => <li key={index}>{item}</li>)}
// ❌ BAD: Random values
{items.map(item => <li key={Math.random()}>{item}</li>)}
Never use
Math.random() or Date.now() as keys! These change on every render, causing React to recreate all elements, destroying component state and causing performance issues.The Index Key Problem
Using array index as key causes bugs when the list order changes:Copy
// ❌ PROBLEMATIC - State gets mixed up when reordering
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Build project' }
]);
const moveToTop = (id) => {
const todo = todos.find(t => t.id === id);
setTodos([todo, ...todos.filter(t => t.id !== id)]);
};
return (
<ul>
{todos.map((todo, index) => (
// Using index as key - BAD when reordering!
<TodoItem key={index} todo={todo} />
))}
</ul>
);
}
// ✅ CORRECT - Use unique ID
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
Filtering Lists
Copy
function FilterableProductList() {
const [filter, setFilter] = useState('all');
const products = [
{ id: 1, name: 'iPhone', category: 'electronics', price: 999 },
{ id: 2, name: 'T-Shirt', category: 'clothing', price: 29 },
{ id: 3, name: 'MacBook', category: 'electronics', price: 1999 },
{ id: 4, name: 'Jeans', category: 'clothing', price: 79 }
];
const filteredProducts = filter === 'all'
? products
: products.filter(p => p.category === filter);
return (
<div>
<div className="filters">
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('electronics')}>Electronics</button>
<button onClick={() => setFilter('clothing')}>Clothing</button>
</div>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
{filteredProducts.length === 0 && (
<p>No products match this filter.</p>
)}
</div>
);
}
Sorting Lists
Copy
function SortableTable() {
const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
const users = [
{ id: 1, name: 'Charlie', age: 30, email: '[email protected]' },
{ id: 2, name: 'Alice', age: 25, email: '[email protected]' },
{ id: 3, name: 'Bob', age: 35, email: '[email protected]' }
];
const sortedUsers = [...users].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
const handleSort = (key) => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
}));
};
const SortIcon = ({ column }) => {
if (sortConfig.key !== column) return '↕️';
return sortConfig.direction === 'asc' ? '↑' : '↓';
};
return (
<table>
<thead>
<tr>
<th onClick={() => handleSort('name')}>
Name <SortIcon column="name" />
</th>
<th onClick={() => handleSort('age')}>
Age <SortIcon column="age" />
</th>
<th onClick={() => handleSort('email')}>
Email <SortIcon column="email" />
</th>
</tr>
</thead>
<tbody>
{sortedUsers.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
);
}
Search and Filter Combined
Copy
function SearchableList() {
const [searchTerm, setSearchTerm] = useState('');
const items = [
{ id: 1, title: 'React Fundamentals', tags: ['react', 'javascript'] },
{ id: 2, title: 'Node.js Basics', tags: ['node', 'javascript'] },
{ id: 3, title: 'CSS Grid Layout', tags: ['css', 'design'] },
{ id: 4, title: 'TypeScript Guide', tags: ['typescript', 'javascript'] }
];
const filteredItems = items.filter(item => {
const matchesTitle = item.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesTags = item.tags.some(tag =>
tag.toLowerCase().includes(searchTerm.toLowerCase())
);
return matchesTitle || matchesTags;
});
return (
<div>
<input
type="text"
placeholder="Search by title or tag..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<p>{filteredItems.length} results found</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>
<strong>{item.title}</strong>
<div className="tags">
{item.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
</li>
))}
</ul>
</div>
);
}
Nested Lists
Copy
function CategoryList() {
const categories = [
{
id: 1,
name: 'Electronics',
products: [
{ id: 101, name: 'Laptop' },
{ id: 102, name: 'Phone' }
]
},
{
id: 2,
name: 'Clothing',
products: [
{ id: 201, name: 'T-Shirt' },
{ id: 202, name: 'Jeans' }
]
}
];
return (
<div>
{categories.map(category => (
<div key={category.id} className="category">
<h2>{category.name}</h2>
<ul>
{category.products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
Keys only need to be unique among siblings. In the example above,
product.id can be the same number as category.id because they’re in different levels.Rendering Nothing for Some Items
Usefilter() before map(), or return null from map:
Copy
function ActiveUsersList({ users }) {
// Method 1: Filter then map (preferred)
return (
<ul>
{users
.filter(user => user.isActive)
.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function ConditionalList({ items }) {
// Method 2: Return null for items to skip
return (
<ul>
{items.map(item => {
if (item.isHidden) return null;
return <li key={item.id}>{item.name}</li>;
})}
</ul>
);
}
Empty States
Always handle the case when a list is empty:Copy
function UserList({ users }) {
if (users.length === 0) {
return (
<div className="empty-state">
<img src="/empty.svg" alt="No users" />
<h3>No users found</h3>
<p>Try adjusting your search or filters.</p>
<button>Clear filters</button>
</div>
);
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Performance: Extracting List Items
For complex lists, extract the item into its own component:Copy
// ✅ Good - Extracted component
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
</div>
);
}
function ProductGrid({ products, onAddToCart }) {
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
}
Performance Tip: When a list item is its own component, you can wrap it with
React.memo() to prevent re-renders when other items change.Copy
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
// Only re-renders if product or onAddToCart changes
return (/* ... */);
});
🎯 Practice Exercises
Exercise 1: Todo List with CRUD
Exercise 1: Todo List with CRUD
Copy
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build project', completed: false }
]);
const [input, setInput] = useState('');
const addTodo = () => {
if (!input.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const pendingCount = todos.filter(t => !t.completed).length;
return (
<div>
<h1>Todos ({pendingCount} pending)</h1>
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="Add todo..."
/>
<button onClick={addTodo}>Add</button>
</div>
{todos.length === 0 ? (
<p>No todos yet!</p>
) : (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>🗑️</button>
</li>
))}
</ul>
)}
</div>
);
}
Exercise 2: Filterable Contact List
Exercise 2: Filterable Contact List
Copy
function ContactList() {
const [search, setSearch] = useState('');
const [filter, setFilter] = useState('all'); // all, favorites
const contacts = [
{ id: 1, name: 'Alice Johnson', phone: '555-0101', favorite: true },
{ id: 2, name: 'Bob Smith', phone: '555-0102', favorite: false },
{ id: 3, name: 'Charlie Brown', phone: '555-0103', favorite: true },
{ id: 4, name: 'Diana Ross', phone: '555-0104', favorite: false }
];
const filteredContacts = contacts
.filter(c => filter === 'all' || c.favorite)
.filter(c =>
c.name.toLowerCase().includes(search.toLowerCase()) ||
c.phone.includes(search)
);
return (
<div>
<input
type="text"
placeholder="Search contacts..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<div>
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'favorites' ? 'active' : ''}
onClick={() => setFilter('favorites')}
>
⭐ Favorites
</button>
</div>
<ul>
{filteredContacts.map(contact => (
<li key={contact.id}>
{contact.favorite && '⭐'} {contact.name} - {contact.phone}
</li>
))}
</ul>
{filteredContacts.length === 0 && (
<p>No contacts found.</p>
)}
</div>
);
}
Exercise 3: Reorderable List
Exercise 3: Reorderable List
Copy
function ReorderableList() {
const [items, setItems] = useState([
{ id: 1, text: 'First item' },
{ id: 2, text: 'Second item' },
{ id: 3, text: 'Third item' },
{ id: 4, text: 'Fourth item' }
]);
const moveUp = (index) => {
if (index === 0) return;
const newItems = [...items];
[newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
setItems(newItems);
};
const moveDown = (index) => {
if (index === items.length - 1) return;
const newItems = [...items];
[newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]];
setItems(newItems);
};
return (
<ul>
{items.map((item, index) => (
<li key={item.id} style={{ display: 'flex', gap: '10px', padding: '8px' }}>
<span>{item.text}</span>
<button
onClick={() => moveUp(index)}
disabled={index === 0}
>
↑
</button>
<button
onClick={() => moveDown(index)}
disabled={index === items.length - 1}
>
↓
</button>
</li>
))}
</ul>
);
}
Summary
| Concept | Description |
|---|---|
map() | Transform array data into React elements |
key | Unique identifier for list items (required) |
| ID as key | Best practice - use database IDs or stable unique identifiers |
| Index as key | Only for static lists that never reorder |
filter() | Filter items before rendering |
sort() | Always use [...arr].sort() (don’t mutate) |
| Empty states | Handle zero results gracefully |
| Nested lists | Keys only need to be unique among siblings |
| Extracted components | Better organization and potential performance gains |
Next Steps
In the next chapter, you’ll learn about Forms & Controlled Components — handling user input effectively!