Lists and Keys
Rendering Lists
In React, you can render lists using JavaScript's map() function to transform arrays into JSX elements.
Basic List Rendering
function UserList() {
const users = ['Alice', 'Bob', 'Charlie'];
return (
<ul>
{users.map((user, index) => (
<li key={index}>{user}</li>
))}
</ul>
);
}
Rendering Objects
function TodoList() {
const todos = [
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build a project', completed: false },
{ id: 3, text: 'Deploy app', completed: true },
];
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input type='checkbox' checked={todo.completed} />
<span>{todo.text}</span>
</li>
))}
</ul>
);
}
Component Lists
function ProductCard({ product }) {
return (
<div className='product-card'>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
function ProductList({ products }) {
return (
<div className='product-grid'>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Understanding Keys
Keys help React identify which items have changed, are added, or are removed. Keys should be stable, unique, and consistent across re-renders.
Why Keys Matter
// ❌ Without keys - React doesn't know which items changed
function List({ items }) {
return (
<ul>
{items.map(item => (
<li>{item.name}</li> // Missing key!
))}
</ul>
);
}
// ✅ With keys - React can track each item
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
What Happens Without Keys
// Initial render: ['A', 'B', 'C']
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
// After removing 'B': ['A', 'C']
// ❌ Without keys: React reuses DOM nodes incorrectly
// React thinks 'C' became 'B' and removes last item
// ✅ With keys: React correctly identifies 'B' was removed
<ul>
<li key="a">A</li>
<li key="c">C</li>
</ul>
Key Rules
- Must be unique among siblings (not globally)
- Must be stable (don't change between renders)
- Must be predictable (not random)
// ❌ BAD - index as key (problematic when list changes)
{
items.map((item, index) => <li key={index}>{item.name}</li>);
}
// ❌ BAD - random keys (will cause re-renders)
{
items.map(item => <li key={Math.random()}>{item.name}</li>);
}
// ❌ BAD - unstable keys (new object each render)
{
items.map(item => <li key={Date.now()}>{item.name}</li>);
}
// ✅ GOOD - stable unique ID
{
items.map(item => <li key={item.id}>{item.name}</li>);
}
// ✅ GOOD - composite key when no ID exists
{
items.map(item => (
<li key={`${item.userId}-${item.category}`}>{item.name}</li>
));
}
When Index as Key is OK
// ✅ OK to use index when:
// 1. List is static (never reordered/filtered)
// 2. Items don't have stable IDs
// 3. List is never filtered or reordered
function StaticList() {
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
return (
<ul>
{MONTHS.map((month, index) => (
<li key={index}>{month}</li>
))}
</ul>
);
}
Key Problems Demonstration
Problem: State Gets Mixed Up
function TodoItem({ todo }) {
const [isEditing, setIsEditing] = useState(false);
return (
<li>
{isEditing ? (
<input defaultValue={todo.text} />
) : (
<span>{todo.text}</span>
)}
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Save' : 'Edit'}
</button>
</li>
);
}
function TodoList({ todos }) {
// ❌ Using index - editing state gets mixed up when items reorder
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
</ul>
);
// ✅ Using ID - state stays with correct item
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
Problem: Performance Issues
function ExpensiveListItem({ item }) {
// Expensive computation or heavy rendering
const processedData = useMemo(() => {
return expensiveCalculation(item);
}, [item]);
return <div>{processedData}</div>;
}
function List({ items }) {
// ❌ Index as key - items unnecessarily recalculate when list changes
return items.map((item, index) => (
<ExpensiveListItem key={index} item={item} />
));
// ✅ Stable key - only changed items recalculate
return items.map(item => <ExpensiveListItem key={item.id} item={item} />);
}
Advanced List Patterns
Filtering Lists
function ProductList({ products, filter }) {
const filteredProducts = products.filter(product => {
if (filter === 'all') return true;
if (filter === 'inStock') return product.stock > 0;
if (filter === 'onSale') return product.discount > 0;
return true;
});
return (
<div>
{filteredProducts.length === 0 ? (
<EmptyState message='No products found' />
) : (
filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))
)}
</div>
);
}
Sorting Lists
function UserTable({ users, sortBy }) {
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
if (sortBy === 'age') {
return a.age - b.age;
}
return 0;
});
}, [users, sortBy]);
return (
<table>
<tbody>
{sortedUsers.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
</tr>
))}
</tbody>
</table>
);
}
Grouping Lists
function GroupedList({ items }) {
const groupedByCategory = items.reduce((groups, item) => {
const category = item.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(item);
return groups;
}, {});
return (
<div>
{Object.entries(groupedByCategory).map(([category, items]) => (
<div key={category}>
<h2>{category}</h2>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
Nested Lists
function CommentThread({ comments }) {
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>
<CommentItem comment={comment} />
{comment.replies && comment.replies.length > 0 && (
<CommentThread comments={comment.replies} />
)}
</li>
))}
</ul>
);
}
Paginated Lists
function PaginatedList({ items, itemsPerPage = 10 }) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const paginatedItems = items.slice(startIndex, startIndex + itemsPerPage);
return (
<div>
<ul>
{paginatedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className='pagination'>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span>
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
);
}
Infinite Scroll
function InfiniteList({ loadMore }) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const observerRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && !loading) {
loadMoreItems();
}
},
{ threshold: 0.1 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [loading]);
const loadMoreItems = async () => {
setLoading(true);
const newItems = await loadMore();
setItems(prev => [...prev, ...newItems]);
setLoading(false);
};
return (
<div>
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
<div ref={observerRef}>{loading && <Spinner />}</div>
</div>
);
}
Virtual Scrolling / Windowing
For very large lists, render only visible items.
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width='100%'
>
{Row}
</FixedSizeList>
);
}
Dynamic Lists with CRUD Operations
function TaskList() {
const [tasks, setTasks] = useState([
{ id: '1', text: 'Task 1', done: false },
{ id: '2', text: 'Task 2', done: false },
]);
const [newTask, setNewTask] = useState('');
// Create
const addTask = () => {
if (!newTask.trim()) return;
const task = {
id: crypto.randomUUID(),
text: newTask,
done: false,
};
setTasks(prev => [...prev, task]);
setNewTask('');
};
// Update
const toggleTask = id => {
setTasks(prev =>
prev.map(task => (task.id === id ? { ...task, done: !task.done } : task))
);
};
// Delete
const deleteTask = id => {
setTasks(prev => prev.filter(task => task.id !== id));
};
// Reorder
const moveTask = (id, direction) => {
const index = tasks.findIndex(t => t.id === id);
if (
(direction === 'up' && index === 0) ||
(direction === 'down' && index === tasks.length - 1)
) {
return;
}
const newTasks = [...tasks];
const newIndex = direction === 'up' ? index - 1 : index + 1;
[newTasks[index], newTasks[newIndex]] = [
newTasks[newIndex],
newTasks[index],
];
setTasks(newTasks);
};
return (
<div>
<div>
<input
value={newTask}
onChange={e => setNewTask(e.target.value)}
onKeyPress={e => e.key === 'Enter' && addTask()}
/>
<button onClick={addTask}>Add</button>
</div>
<ul>
{tasks.map(task => (
<li key={task.id}>
<input
type='checkbox'
checked={task.done}
onChange={() => toggleTask(task.id)}
/>
<span
style={{ textDecoration: task.done ? 'line-through' : 'none' }}
>
{task.text}
</span>
<button onClick={() => moveTask(task.id, 'up')}>↑</button>
<button onClick={() => moveTask(task.id, 'down')}>↓</button>
<button onClick={() => deleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Drag and Drop Lists
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
function DraggableList() {
const [items, setItems] = useState([
{ id: '1', content: 'Item 1' },
{ id: '2', content: 'Item 2' },
{ id: '3', content: 'Item 3' },
]);
const handleDragEnd = result => {
if (!result.destination) return;
const newItems = Array.from(items);
const [removed] = newItems.splice(result.source.index, 1);
newItems.splice(result.destination.index, 0, removed);
setItems(newItems);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId='list'>
{provided => (
<ul {...provided.droppableProps} ref={provided.innerRef}>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{provided => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.content}
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
TypeScript with Lists
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface UserListProps {
users: User[];
onUserClick: (userId: string) => void;
filter?: (user: User) => boolean;
}
const UserList: React.FC<UserListProps> = ({ users, onUserClick, filter }) => {
const filteredUsers = filter ? users.filter(filter) : users;
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id} onClick={() => onUserClick(user.id)}>
<span>{user.name}</span>
<span>{user.email}</span>
<span>{user.role}</span>
</li>
))}
</ul>
);
};
// Usage
<UserList
users={users}
onUserClick={id => console.log(id)}
filter={user => user.role === 'admin'}
/>;
Performance Optimization
// 1. Use React.memo for list items
const ListItem = React.memo(({ item, onDelete }) => {
console.log('Rendering item:', item.id);
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
);
});
// 2. Use stable keys
function List({ items }) {
return items.map(item => (
<ListItem key={item.id} item={item} /> // ✅ Stable key
));
}
// 3. Memoize filtered/sorted lists
function FilteredList({ items, filter }) {
const filteredItems = useMemo(
() => items.filter(item => item.category === filter),
[items, filter]
);
return filteredItems.map(item => <ListItem key={item.id} item={item} />);
}
// 4. Use useCallback for handlers
function List({ items }) {
const handleDelete = useCallback(id => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return items.map(item => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
));
}
Best Practices
- Always use keys - Never omit keys in lists
- Use stable, unique keys - Prefer ID over index
- Don't use index - Unless list is static and never reordered
- Keys only need to be unique among siblings - Not globally
- Don't mutate arrays - Use immutable patterns
- Optimize large lists - Use virtualization/windowing
- Memoize expensive operations - Filter, sort, map
- Use TypeScript - Type your list items
Common Mistakes
// ❌ Missing key
{
items.map(item => <div>{item.name}</div>);
}
// ❌ Non-unique keys
{
items.map(item => <div key='same'>{item.name}</div>);
}
// ❌ Index when list changes
{
items.map((item, i) => <div key={i}>{item.name}</div>);
}
// ❌ Mutating original array
items.sort().map(item => <div key={item.id}>{item.name}</div>);
// ✅ Correct approach
{
items.map(item => <div key={item.id}>{item.name}</div>);
}
// ✅ Create new array before sorting
{
[...items].sort().map(item => <div key={item.id}>{item.name}</div>);
}
Practice Exercises
- Build a searchable, sortable, filterable product list
- Create a todo list with add, edit, delete, and reorder functionality
- Implement infinite scroll with real API data
- Build a virtual scrolling list with 10,000+ items
- Create a drag-and-drop kanban board
Next Steps
- Continue practicing list rendering patterns
- Build projects with dynamic data