Rendering Lists
Rendering Lists in React
Lists are common in web applications. React provides powerful ways to render lists of data efficiently using JavaScript's array methods and the key prop.
Basic List Rendering
Use the map()
method to transform arrays into lists of elements:
function NumberList() {
const numbers = [1, 2, 3, 4, 5];
return (
<ul>
{numbers.map(number => (
<li key={number}>{number}</li>
))}
</ul>
);
}
Rendering Complex Data
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 (
<div className="user-list">
{users.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
);
}
Extracting List Item Components
Keep your code organized by extracting list items into separate components:
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
Conditional Rendering in Lists
Filtering Items
function TaskList({ tasks, showCompleted }) {
const filteredTasks = showCompleted
? tasks
: tasks.filter(task => !task.completed);
return (
<ul>
{filteredTasks.map(task => (
<li key={task.id}>
{task.title}
</li>
))}
</ul>
);
}
Conditional Elements
function ProductList({ products }) {
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
{product.onSale && <span className="sale-badge">SALE!</span>}
{product.stock === 0 && <span className="out-of-stock">Out of Stock</span>}
</div>
))}
</div>
);
}
Empty States
Handle empty lists gracefully:
function ItemList({ items, title }) {
if (items.length === 0) {
return (
<div className="empty-state">
<p>No {title.toLowerCase()} found.</p>
<button>Add your first {title.toLowerCase()}</button>
</div>
);
}
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Nested Lists
Render lists within lists:
function CategoryList({ categories }) {
return (
<div className="categories">
{categories.map(category => (
<div key={category.id} className="category">
<h2>{category.name}</h2>
<ul>
{category.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
// Example data
const categories = [
{
id: 1,
name: 'Fruits',
items: [
{ id: 11, name: 'Apple' },
{ id: 12, name: 'Banana' }
]
},
{
id: 2,
name: 'Vegetables',
items: [
{ id: 21, name: 'Carrot' },
{ id: 22, name: 'Broccoli' }
]
}
];
Sorting Lists
function SortableList({ items }) {
const [sortBy, setSortBy] = useState('name');
const [sortOrder, setSortOrder] = useState('asc');
const sortedItems = [...items].sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
return (
<div>
<div className="sort-controls">
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="date">Date</option>
</select>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
{sortOrder === 'asc' ? '↑' : '↓'}
</button>
</div>
<ul>
{sortedItems.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
</div>
);
}
Pagination
Handle large lists with pagination:
function PaginatedList({ items, itemsPerPage = 10 }) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(items.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentItems = items.slice(startIndex, endIndex);
return (
<div>
<ul>
{currentItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
);
}
Infinite Scroll
Load more items as user scrolls:
function InfiniteList() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
if (newItems.length === 0) {
setHasMore(false);
} else {
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
}
} finally {
setLoading(false);
}
}, [page, loading, hasMore]);
useEffect(() => {
loadMore();
}, []);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
loadMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [loadMore]);
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{loading && <p>Loading more...</p>}
{!hasMore && <p>No more items</p>}
</div>
);
}
Virtualized Lists
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={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Dynamic Lists
Add, remove, and update list items:
function DynamicList() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' }
]);
const [newItem, setNewItem] = useState('');
const addItem = () => {
if (newItem.trim()) {
setItems([...items, {
id: Date.now(),
text: newItem
}]);
setNewItem('');
}
};
const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
const updateItem = (id, newText) => {
setItems(items.map(item =>
item.id === id ? { ...item, text: newText } : item
));
};
return (
<div>
<div className="add-item">
<input
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
placeholder="Add new item"
/>
<button onClick={addItem}>Add</button>
</div>
<ul>
{items.map(item => (
<li key={item.id}>
<input
value={item.text}
onChange={(e) => updateItem(item.id, e.target.value)}
/>
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
Grouped Lists
Group items by a property:
function GroupedList({ items }) {
const groupedItems = items.reduce((groups, item) => {
const group = item.category;
if (!groups[group]) groups[group] = [];
groups[group].push(item);
return groups;
}, {});
return (
<div>
{Object.entries(groupedItems).map(([category, items]) => (
<div key={category} className="group">
<h3>{category}</h3>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
Performance Tips
1. Use React.memo for List Items
const ListItem = React.memo(({ item, onUpdate }) => {
console.log(`Rendering ${item.id}`);
return (
<li>
{item.name}
<button onClick={() => onUpdate(item.id)}>Update</button>
</li>
);
});
2. Stable Handler References
function OptimizedList({ items }) {
const handleUpdate = useCallback((id) => {
// Update logic
}, []);
return (
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onUpdate={handleUpdate}
/>
))}
</ul>
);
}
3. Avoid Index as Key
// ❌ Bad - can cause issues with reordering
{items.map((item, index) => <li key={index}>{item}</li>)}
// ✅ Good - stable unique identifier
{items.map(item => <li key={item.id}>{item.name}</li>)}
Common Patterns
Search and Filter
function SearchableList({ items }) {
const [search, setSearch] = useState('');
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Best Practices
- Always use keys - Unique and stable identifiers
- Extract list items - Create separate components
- Handle empty states - Show meaningful messages
- Optimize performance - Use React.memo and useCallback
- Avoid array index as key - Unless list is static
- Consider virtualization - For very large lists
- Implement loading states - For async data
- Add error handling - For data fetching
Lists are fundamental to React applications. Master these patterns and you'll be able to handle any list rendering scenario!