Lists and Keys
Rendering Lists in React
Lists are one of the most common patterns in React. You’ll use .map() to transform arrays into JSX elements.
function FruitList() {
const fruits = ['Apple', 'Banana', 'Orange'];
return (
<ul>
{fruits.map(fruit => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}
The .map() Method
.map() transforms each array item into JSX:
Basic List
function NumberList() {
const numbers = [1, 2, 3, 4, 5];
return (
<ul>
{numbers.map(number => (
<li key={number}>Number: {number}</li>
))}
</ul>
);
}
List of Objects
function UserList() {
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];
return (
<div>
{users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
);
}
With Components
function ProductList({ products }) {
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
Keys - Why They Matter
Keys help React identify which items changed, were added, or were removed:
Without Keys (Bad)
// ❌ React will warn about missing keys
function List() {
const items = ['A', 'B', 'C'];
return (
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
);
}
// Warning: Each child in a list should have a unique "key" prop
With Keys (Good)
// ✅ Keys help React track items
function List() {
const items = [
{ id: 1, text: 'A' },
{ id: 2, text: 'B' },
{ id: 3, text: 'C' }
];
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
Why Keys Are Important
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build app', done: false }
]);
function toggleTodo(id) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
// Without keys, React might update the wrong checkbox!
Choosing Good Keys
✅ Use Unique IDs
// Best - use database IDs
function UserList({ users }) {
return users.map(user => (
<UserCard key={user.id} user={user} />
));
}
// Good - use stable unique identifiers
function PostList({ posts }) {
return posts.map(post => (
<Post key={post.slug} post={post} />
));
}
⚠️ Index as Key (Only When Safe)
// OK - static list that never changes
function MonthList() {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
return months.map((month, index) => (
<li key={index}>{month}</li>
));
}
// ❌ Bad - dynamic list with reordering/filtering
function TodoList({ todos }) {
return todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
// Bugs when todos are added/removed/reordered!
));
}
Generate IDs for Items Without Them
import { nanoid } from 'nanoid'; // or use crypto.randomUUID()
function TodoApp() {
const [todos, setTodos] = useState([]);
function addTodo(text) {
const newTodo = {
id: nanoid(), // Generate unique ID
text,
done: false
};
setTodos([...todos, newTodo]);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Filtering Lists
Show a subset of items based on criteria:
Basic Filter
function ProductList({ products, showOnSale }) {
const displayProducts = showOnSale
? products.filter(p => p.onSale)
: products;
return (
<div>
{displayProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Search Filter
function UserList() {
const [searchTerm, setSearchTerm] = useState('');
const users = [
{ id: 1, name: 'Alice Johnson' },
{ id: 2, name: 'Bob Smith' },
{ id: 3, name: 'Charlie Brown' }
];
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search users..."
/>
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Multiple Filters
function ProductList({ products }) {
const [filters, setFilters] = useState({
category: 'all',
minPrice: 0,
maxPrice: Infinity,
inStock: false
});
const filteredProducts = products
.filter(p => filters.category === 'all' || p.category === filters.category)
.filter(p => p.price >= filters.minPrice)
.filter(p => p.price <= filters.maxPrice)
.filter(p => !filters.inStock || p.stock > 0);
return (
<div>
{/* Filter controls */}
<div>
<select
value={filters.category}
onChange={(e) => setFilters({ ...filters, category: e.target.value })}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => setFilters({ ...filters, inStock: e.target.checked })}
/>
In Stock Only
</label>
</div>
{/* Product list */}
<div>
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
Sorting Lists
Reorder items based on criteria:
Basic Sort
function UserList({ users }) {
const [sortBy, setSortBy] = useState('name');
const sortedUsers = [...users].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
if (sortBy === 'age') {
return a.age - b.age;
}
return 0;
});
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="age">Sort by Age</option>
</select>
<ul>
{sortedUsers.map(user => (
<li key={user.id}>
{user.name} ({user.age})
</li>
))}
</ul>
</div>
);
}
Sort Direction
function ProductList({ products }) {
const [sortField, setSortField] = useState('name');
const [sortDirection, setSortDirection] = useState('asc');
const sortedProducts = [...products].sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
let comparison = 0;
if (typeof aVal === 'string') {
comparison = aVal.localeCompare(bVal);
} else {
comparison = aVal - bVal;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
return (
<div>
<select value={sortField} onChange={(e) => setSortField(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
<button onClick={() => setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')}>
{sortDirection === 'asc' ? '↑' : '↓'}
</button>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Combining Filter and Sort
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', priority: 'high', done: false },
{ id: 2, text: 'Build app', priority: 'medium', done: false },
{ id: 3, text: 'Deploy', priority: 'low', done: true }
]);
const [filter, setFilter] = useState('all'); // all, active, done
const [sortBy, setSortBy] = useState('priority');
const processedTodos = todos
// Filter first
.filter(todo => {
if (filter === 'active') return !todo.done;
if (filter === 'done') return todo.done;
return true;
})
// Then sort
.sort((a, b) => {
if (sortBy === 'priority') {
const priorityOrder = { high: 1, medium: 2, low: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
}
return 0;
});
return (
<div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('done')}>Done</button>
</div>
<ul>
{processedTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => {/* toggle */}}
/>
{todo.text} ({todo.priority})
</li>
))}
</ul>
</div>
);
}
Nested Lists
Lists inside lists:
Simple Nested List
function CategoryList({ categories }) {
return (
<div>
{categories.map(category => (
<div key={category.id}>
<h2>{category.name}</h2>
<ul>
{category.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
Nested Components
function CommentThread({ comments }) {
return (
<div>
{comments.map(comment => (
<div key={comment.id} className="comment">
<p>{comment.text}</p>
<span>{comment.author}</span>
{comment.replies && comment.replies.length > 0 && (
<div className="replies">
{comment.replies.map(reply => (
<div key={reply.id} className="reply">
<p>{reply.text}</p>
<span>{reply.author}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
);
}
Empty States
Handle empty lists gracefully:
function TodoList({ todos }) {
if (todos.length === 0) {
return (
<div className="empty-state">
<p>No todos yet!</p>
<button>Add your first todo</button>
</div>
);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// Or inline
function TodoList({ todos }) {
return (
<div>
{todos.length === 0 ? (
<p>No todos yet!</p>
) : (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</div>
);
}
Common Patterns
1. Add/Remove from List
function ShoppingList() {
const [items, setItems] = useState([]);
const [input, setInput] = useState('');
function addItem() {
if (input.trim()) {
setItems([...items, { id: Date.now(), text: input }]);
setInput('');
}
}
function removeItem(id) {
setItems(items.filter(item => item.id !== id));
}
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addItem()}
/>
<button onClick={addItem}>Add</button>
<ul>
{items.map(item => (
<li key={item.id}>
{item.text}
<button onClick={() => removeItem(item.id)}>✕</button>
</li>
))}
</ul>
</div>
);
}
2. Update Item in List
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build app', done: false }
]);
function toggleTodo(id) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}
function editTodo(id, newText) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
3. Pagination
function UserList({ users }) {
const [page, setPage] = useState(1);
const perPage = 10;
const startIndex = (page - 1) * perPage;
const endIndex = startIndex + perPage;
const pageUsers = users.slice(startIndex, endIndex);
const totalPages = Math.ceil(users.length / perPage);
return (
<div>
<ul>
{pageUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<div>
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
>
Next
</button>
</div>
</div>
);
}
Best Practices
✅ DO:
// Use unique, stable IDs as keys
<li key={item.id}>{item.name}</li>
// Filter/sort before mapping
const filtered = items.filter(i => i.active);
return filtered.map(item => <Item key={item.id} />);
// Extract list items to components
{users.map(user => <UserCard key={user.id} user={user} />)}
// Handle empty lists
{items.length === 0 ? <EmptyState /> : <ItemList items={items} />}
// Use [...array] when sorting (don't mutate)
const sorted = [...items].sort((a, b) => a.value - b.value);
❌ DON’T:
// Don't use index as key for dynamic lists
{todos.map((todo, index) => <li key={index}>{todo.text}</li>)}
// Don't mutate the array
items.sort(); // ❌ Mutates original
return items.map(i => <Item key={i.id} />);
// Don't forget keys
{items.map(item => <li>{item.text}</li>)} // Warning!
// Don't use random values as keys
{items.map(item => <li key={Math.random()}>{item.text}</li>)}
// Don't use non-unique keys
{items.map(item => <li key={item.category}>{item.name}</li>)}
Summary
Basic List Rendering:
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
Keys:
- Required for list items
- Must be unique among siblings
- Should be stable (don’t use
Math.random()) - Best: Use IDs from data
- OK: Use index only for static lists
Common Operations:
// Filter
const filtered = items.filter(i => i.active);
// Sort (don't mutate!)
const sorted = [...items].sort((a, b) => a.value - b.value);
// Add
setItems([...items, newItem]);
// Remove
setItems(items.filter(i => i.id !== removeId));
// Update
setItems(items.map(i => i.id === updateId ? { ...i, done: true } : i));
Empty State:
{items.length === 0 ? (
<EmptyMessage />
) : (
items.map(item => <Item key={item.id} />)
)}
Next Article: Forms in React