📜 Lists and Keys
Real applications work with collections of data - users, products, posts, comments, messages, and more. Rendering lists efficiently is a fundamental React skill. But there's a catch: React needs a special "key" prop to track items in lists. Get it wrong, and you'll have bugs that are hard to debug. Get it right, and your lists will be fast, predictable, and bug-free. In this lesson, you'll master rendering lists, understand why keys matter, and learn how to perform CRUD operations on dynamic lists. Let's make lists awesome! 🚀
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Render arrays of data using map()
- Understand why the key prop is critical
- Choose appropriate keys for different scenarios
- Implement CRUD operations on lists
- Use filter(), reduce(), and other array methods
- Type arrays properly with TypeScript
- Handle empty lists gracefully
- Optimize list rendering performance
- Build dynamic, interactive lists
Estimated Time: 70-85 minutes
Project: Build an enhanced todo list with filtering, sorting, and search
📑 In This Lesson
📋 Rendering Lists
In React, you render lists by transforming arrays of data into arrays of JSX elements using JavaScript's map() function.
Basic List Rendering
Simple Array to JSX
const SimpleList: React.FC = () => {
const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
};
// Renders:
// • Apple
// • Banana
// • Cherry
// • Date
How map() Works
Understanding the Transformation
// What we start with
const numbers = [1, 2, 3];
// map() transforms each item
const doubled = numbers.map(num => num * 2);
// [2, 4, 6]
// In React, we transform data into JSX
const listItems = numbers.map(num => <li>{num}</li>);
// [<li>1</li>, <li>2</li>, <li>3</li>]
// JSX expression:
<ul>{listItems}</ul>
Rendering Objects
Lists of Objects
interface User {
id: number;
name: string;
email: string;
}
const UserList: React.FC = () => {
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
{ id: 3, name: 'Charlie', email: '[email protected]' }
];
return (
<div>
{users.map(user => (
<div key={user.id} style={{ padding: '1rem', border: '1px solid #ddd', marginBottom: '0.5rem' }}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
);
};
Common Pattern: Extract to Component
Keep It Clean
interface User {
id: number;
name: string;
email: string;
}
// Individual item component
interface UserCardProps {
user: User;
}
const UserCard: React.FC<UserCardProps> = ({ user }) => (
<div style={{ padding: '1rem', border: '1px solid #ddd', marginBottom: '0.5rem' }}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
// List component
const UserList: React.FC = () => {
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
{ id: 3, name: 'Charlie', email: '[email protected]' }
];
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
};
✅ This is cleaner and more maintainable!
💡 Key Insight
The pattern is always the same:
- Have an array of data
- Use
.map()to transform each item - Return JSX for each item
- Add a unique
keyprop
🔑 The Key Prop
The key prop is one of React's most misunderstood features. Let's demystify it!
📖 Definition
Key Prop: A special attribute that helps React identify which items have changed, been added, or been removed. Keys give list items a stable identity.
Why Keys Matter
What Happens Without Keys?
// ❌ Bad: No keys
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
// React console warning:
// Warning: Each child in a list should have a unique "key" prop.
Without keys, React doesn't know which element is which. This causes:
- ⚠️ React warns in console
- ⚠️ Potential rendering bugs
- ⚠️ Poor performance on updates
- ⚠️ State can get mixed up
How Keys Work
graph TD
A[Array Changes] --> B{React compares keys}
B -->|Same key| C[Update element]
B -->|New key| D[Create new element]
B -->|Key removed| E[Delete element]
B -->|Key moved| F[Move element]
style C fill:#4CAF50,color:#fff
style D fill:#2196F3,color:#fff
style E fill:#f44336,color:#fff
style F fill:#FF9800,color:#fff
Keys Must Be Unique Among Siblings
Uniqueness Rules
// ✅ Good: Unique keys among siblings
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
// ✅ Also good: Different lists can reuse keys
<div>
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
🎨 How Keys Help React Track Items
</ul>
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
// ❌ Bad: Duplicate keys in same list
<ul>
<li key="1">Item 1</li>
<li key="1">Item 2</li> {/* Duplicate key! */}
</ul>
The Index as Key Problem
⚠️ Warning: Using Index as Key
// ⚠️ Problematic in many cases
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
Why it's problematic:
Imagine you have a list: ["A", "B", "C"]
key=0: A
key=1: B
key=2: C
You remove "A". Now you have: ["B", "C"]
key=0: B (was key=1 before!)
key=1: C (was key=2 before!)
React thinks items at key=0 and key=1 changed content, when really you just deleted one item!
When index as key is okay:
- ✅ List is static (never changes)
- ✅ Items are never reordered
- ✅ Items are never deleted
- ✅ Items have no state or controlled inputs
When NOT to use index:
- ❌ Items can be added/removed
- ❌ List can be sorted/filtered
- ❌ Items have input fields
- ❌ Items have local component state
Example: The Bug Index Keys Can Cause
Demonstration of the Problem
interface TodoItemProps {
text: string;
}
const TodoItem: React.FC<TodoItemProps> = ({ text }) => {
const [done, setDone] = useState(false);
return (
<div>
<input
type="checkbox"
checked={done}
onChange={() => setDone(!done)}
/>
{text}
</div>
);
};
// ❌ Using index as key
const BadTodoList: React.FC = () => {
const [todos, setTodos] = useState(['Task A', 'Task B', 'Task C']);
return (
<div>
{todos.map((todo, index) => (
<TodoItem key={index} text={todo} /> {/* Bug! */}
))}
<button onClick={() => setTodos(todos.slice(1))}>
Remove first item
</button>
</div>
);
};
// What happens:
// 1. Check "Task A"
// 2. Remove first item
// 3. Now "Task B" appears checked! (because it has the old key=0)
✅ The Fix: Use Stable IDs
interface Todo {
id: number; // or string
text: string;
}
const GoodTodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Task A' },
{ id: 2, text: 'Task B' },
{ id: 3, text: 'Task C' }
]);
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} text={todo.text} /> {/* ✅ Correct! */}
))}
</div>
);
};
🎯 Choosing Good Keys
A good key is stable, unique, and consistent. Let's learn how to choose keys properly.
Best Key Sources
| Source | When to Use | Example |
|---|---|---|
| Database ID | Data from backend with IDs | key={user.id} |
| UUID/GUID | Generated unique IDs | key={item.uuid} |
| Unique Property | Natural unique identifier | key={user.email} |
| Generated on Creation | Client-side items | key={Date.now()} |
| Composite Key | Multiple properties unique | key={`${cat}-${id}`} |
| Index | Static, never-changing list | key={index} |
Generating Keys for New Items
Pattern: Add ID When Creating
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(), // Simple unique ID
text: text,
completed: false
};
setTodos([...todos, newTodo]);
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
};
Better ID Generation
Using Libraries or Crypto
// Option 1: Use crypto.randomUUID() (modern browsers)
const id = crypto.randomUUID();
// "36b8f84d-df4e-4d49-b662-bcde71a8764f"
// Option 2: Use nanoid library
import { nanoid } from 'nanoid';
const id = nanoid();
// "V1StGXR8_Z5jdHi6B-myT"
// Option 3: Simple counter (if single user)
let nextId = 1;
const generateId = () => nextId++;
const addTodo = (text: string) => {
const newTodo = {
id: generateId(), // or crypto.randomUUID()
text,
completed: false
};
setTodos([...todos, newTodo]);
};
✅ Key Guidelines
- Stable: Same item always has same key
- Unique: No two siblings share a key
- Consistent: Key doesn't change between renders
- Not Random: Don't use
Math.random()as keys!
⚠️ Common Mistakes
// ❌ Bad: Random key (changes every render!)
key={Math.random()}
// ❌ Bad: Non-unique key
key="same-key-for-all"
// ❌ Bad: Duplicate keys
items.map(item => <li key={1}>...</li>)
// ❌ Bad: Complex object as key (must be string/number)
key={{id: 1}}
🎨 Keys Quick Reference
🔧 Array Methods (map, filter, reduce)
JavaScript's array methods are essential for working with lists in React. Let's master the most important ones!
map() - Transform Each Item
Converting Data to JSX
// map() creates a new array by transforming each element
const numbers = [1, 2, 3, 4, 5];
// Example 1: Double each number
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
// Example 2: Convert to JSX
const listItems = numbers.map(n => <li key={n}>{n}</li>);
// [<li>1</li>, <li>2</li>, ...]
// Example 3: Extract properties
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
const names = users.map(user => user.name);
// ['Alice', 'Bob']
const userCards = users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
));
filter() - Select Items
Filtering Lists
// filter() creates a new array with items that pass a test
const numbers = [1, 2, 3, 4, 5, 6];
// Example 1: Get even numbers
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6]
// Example 2: Filter completed todos
interface Todo {
id: number;
text: string;
completed: boolean;
}
const todos: Todo[] = [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build app', completed: false },
{ id: 3, text: 'Deploy', completed: false }
];
const completed = todos.filter(todo => todo.completed);
// [{ id: 1, text: 'Learn React', completed: true }]
const active = todos.filter(todo => !todo.completed);
// [{ id: 2, ... }, { id: 3, ... }]
// Example 3: Search functionality
const searchTerm = 'react';
const searchResults = todos.filter(todo =>
todo.text.toLowerCase().includes(searchTerm.toLowerCase())
);
// [{ id: 1, text: 'Learn React', completed: true }]
reduce() - Compute Single Value
Aggregating Data
// reduce() combines all elements into a single value
const numbers = [1, 2, 3, 4, 5];
// Example 1: Sum
const sum = numbers.reduce((total, num) => total + num, 0);
// 15
// Example 2: Count completed todos
const completedCount = todos.reduce((count, todo) =>
count + (todo.completed ? 1 : 0), 0
);
// Example 3: Calculate total price
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
const cart: CartItem[] = [
{ id: 1, name: 'Book', price: 12.99, quantity: 2 },
{ id: 2, name: 'Pen', price: 1.99, quantity: 5 }
];
const total = cart.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
// 35.93
// Example 4: Group by property
const grouped = todos.reduce((groups, todo) => {
const key = todo.completed ? 'completed' : 'active';
if (!groups[key]) groups[key] = [];
groups[key].push(todo);
return groups;
}, {} as Record<string, Todo[]>);
// { completed: [...], active: [...] }
Chaining Array Methods
Combining Operations
// You can chain methods for powerful transformations
const todos: Todo[] = [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build App', completed: false },
{ id: 3, text: 'Learn TypeScript', completed: true },
{ id: 4, text: 'Deploy', completed: false }
];
// Example 1: Filter then map
const completedTexts = todos
.filter(todo => todo.completed)
.map(todo => todo.text);
// ['Learn React', 'Learn TypeScript']
// Example 2: Filter, map, then render
const ActiveTodoList: React.FC = () => {
return (
<ul>
{todos
.filter(todo => !todo.completed)
.map(todo => (
<li key={todo.id}>{todo.text}</li>
))
}
</ul>
);
};
// Example 3: Search, filter, sort, map
const searchTerm = 'learn';
const searchResults = todos
.filter(todo => todo.text.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => a.text.localeCompare(b.text))
.map(todo => (
<div key={todo.id}>
<strong>{todo.text}</strong>
<span>{todo.completed ? ' ✓' : ''}</span>
</div>
));
Other Useful Array Methods
| Method | Purpose | Example |
|---|---|---|
find() |
Get first matching item | todos.find(t => t.id === 2) |
findIndex() |
Get index of first match | todos.findIndex(t => t.id === 2) |
some() |
Check if any item matches | todos.some(t => t.completed) |
every() |
Check if all items match | todos.every(t => t.completed) |
slice() |
Get portion of array | todos.slice(0, 5) |
sort() |
Sort array (copy first!) | [...todos].sort((a,b) => ...) |
✅ Array Methods Cheat Sheet
- map(): Transform each item → new array
- filter(): Select items that match → new array
- reduce(): Combine all items → single value
- find(): Get first match → single item or undefined
- some(): Any item matches? → boolean
- every(): All items match? → boolean
🎮 Interactive Demo: Array Methods in Action
Watch how map, filter, and reduce transform arrays:
✏️ CRUD Operations
Let's implement Create, Read, Update, and Delete operations on lists!
Complete CRUD Example
Full Todo App with CRUD
import React, { useState } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build App', completed: false }
]);
const [inputText, setInputText] = useState('');
// CREATE - Add new todo
const addTodo = () => {
if (inputText.trim() === '') return;
const newTodo: Todo = {
id: Date.now(),
text: inputText,
completed: false
};
setTodos([...todos, newTodo]);
setInputText('');
};
// READ - Already handled by rendering the list
// UPDATE - Toggle completion
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// UPDATE - Edit text
const updateTodoText = (id: number, newText: string) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
));
};
// DELETE - Remove todo
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// DELETE - Clear completed
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '2rem' }}>
<h1>Todo List</h1>
{/* CREATE */}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
<input
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add a todo..."
style={{ flex: 1, padding: '0.5rem' }}
/>
<button onClick={addTodo} style={{ padding: '0.5rem 1rem' }}>
Add
</button>
</div>
{/* READ - Display list */}
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem',
background: '#f5f5f5',
marginBottom: '0.5rem',
borderRadius: '4px'
}}
>
{/* UPDATE - Toggle */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : '#000'
}}
>
{todo.text}
</span>
🎮 Interactive Demo: CRUD in Action
See Create, Read, Update, Delete operations on a live list:
{/* DELETE */}
<button
onClick={() => deleteTodo(todo.id)}
style={{
background: '#f44336',
color: 'white',
border: 'none',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Delete
</button>
</li>
))}
</ul>
{/* Additional actions */}
<div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-between' }}>
<span>{todos.filter(t => !t.completed).length} items left</span>
<button onClick={clearCompleted}>Clear completed</button>
</div>
</div>
);
};
export default TodoApp;
CRUD Operation Patterns
| Operation | Pattern | Example |
|---|---|---|
| Create (Add) | Spread and append | [...items, newItem] |
| Read (Display) | Map to JSX | items.map(item => <li>...</li>) |
| Update | Map and replace | items.map(i => i.id === id ? updated : i) |
| Delete | Filter out | items.filter(i => i.id !== id) |
Bulk Operations
Operating on Multiple Items
// Select/deselect all
const [todos, setTodos] = useState<Todo[]>([]);
const toggleAll = () => {
const allCompleted = todos.every(todo => todo.completed);
setTodos(todos.map(todo => ({
...todo,
completed: !allCompleted
})));
};
// Delete multiple
const deleteMultiple = (idsToDelete: number[]) => {
setTodos(todos.filter(todo => !idsToDelete.includes(todo.id)));
};
// Update multiple
const markSelectedAsCompleted = (selectedIds: number[]) => {
setTodos(todos.map(todo =>
selectedIds.includes(todo.id)
? { ...todo, completed: true }
: todo
));
};
Optimistic Updates
Update UI Immediately
const deleteTodoWithAPI = async (id: number) => {
// 1. Update UI immediately (optimistic)
const previousTodos = todos;
setTodos(todos.filter(todo => todo.id !== id));
try {
// 2. Send request to server
await api.deleteTodo(id);
// Success! UI is already updated
} catch (error) {
// 3. If error, revert UI
setTodos(previousTodos);
alert('Failed to delete todo');
}
};
📘 Typing Lists with TypeScript
TypeScript makes working with lists safer and more predictable. Let's type everything properly!
Array Type Annotations
Different Ways to Type Arrays
// Method 1: Type[]
const numbers: number[] = [1, 2, 3];
const strings: string[] = ['a', 'b', 'c'];
// Method 2: Array<Type>
const numbers: Array<number> = [1, 2, 3];
const strings: Array<string> = ['a', 'b', 'c'];
// Interface arrays
interface User {
id: number;
name: string;
}
const users: User[] = [];
// or
const users: Array<User> = [];
// Union type arrays
type Status = 'pending' | 'active' | 'completed';
const statuses: Status[] = ['pending', 'active'];
// Array of objects inline
const items: Array<{ id: number; name: string }> = [
{ id: 1, name: 'Item 1' }
];
Typing Props with Lists
List Component Types
interface Todo {
id: number;
text: string;
completed: boolean;
}
// List component props
interface TodoListProps {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
const TodoList: React.FC<TodoListProps> = ({ todos, onToggle, onDelete }) => {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
};
// Item component props
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
};
Typing Array Methods
Type-Safe Transformations
interface Product {
id: number;
name: string;
price: number;
category: string;
}
const products: Product[] = [
{ id: 1, name: 'Book', price: 12.99, category: 'books' },
{ id: 2, name: 'Pen', price: 1.99, category: 'stationery' }
];
// TypeScript infers types in callbacks
const names = products.map(product => product.name);
// Type: string[]
const expensive = products.filter(product => product.price > 10);
// Type: Product[]
const total = products.reduce((sum, product) => sum + product.price, 0);
// Type: number
// Explicit typing when needed
const formatted = products.map((product): string => {
return `${product.name}: $${product.price}`;
});
// Type: string[]
Generic List Components
Reusable Type-Safe Lists
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
}
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={getKey(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage with User type
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
const UserList: React.FC = () => (
<List
items={users}
renderItem={(user) => (
<div>
<strong>{user.name}</strong>
<p>{user.email}</p>
</div>
)}
getKey={(user) => user.id}
/>
);
// Usage with Product type
interface Product {
id: string;
name: string;
price: number;
}
const products: Product[] = [
{ id: 'p1', name: 'Book', price: 12.99 }
];
const ProductList: React.FC = () => (
<List
items={products}
renderItem={(product) => (
<span>{product.name}: ${product.price}</span>
)}
getKey={(product) => product.id}
/>
);
💡 TypeScript Benefits for Lists
- Autocomplete: IDE suggests available properties
- Type safety: Catches errors at compile time
- Refactor confidence: Rename properties safely
- Documentation: Types document what data looks like
- Inference: Often don't need explicit types in callbacks
🗂️ Empty States and Conditional Lists
Handling empty lists gracefully is important for good UX. Let's learn proper empty state patterns.
Checking for Empty Lists
Basic Empty State
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
// Check if empty
if (todos.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '3rem' }}>
<p style={{ fontSize: '3rem' }}>📝</p>
<h3>No todos yet</h3>
<p>Add your first todo to get started!</p>
</div>
);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
};
Conditional Rendering Patterns
Different Empty State Approaches
// Pattern 1: Ternary operator
<div>
{todos.length === 0 ? (
<p>No todos yet!</p>
) : (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)}
</div>
// Pattern 2: Logical && operator
<div>
{todos.length === 0 && <p>No todos yet!</p>}
{todos.length > 0 && (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)}
</div>
// Pattern 3: Early return (component level)
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => {
if (todos.length === 0) {
return <EmptyState />;
}
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
};
Rich Empty States
Engaging Empty State Component
interface EmptyStateProps {
icon?: string;
title: string;
description: string;
action?: {
label: string;
onClick: () => void;
};
}
const EmptyState: React.FC<EmptyStateProps> = ({
icon = '📭',
title,
description,
action
}) => {
return (
<div style={{
textAlign: 'center',
padding: '3rem',
background: '#f5f5f5',
borderRadius: '8px',
margin: '2rem 0'
}}>
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>
{icon}
</div>
<h3 style={{ marginBottom: '0.5rem' }}>{title}</h3>
<p style={{ color: '#666', marginBottom: '1rem' }}>{description}</p>
{action && (
<button
onClick={action.onClick}
style={{
padding: '0.75rem 1.5rem',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem'
}}
>
{action.label}
</button>
)}
</div>
);
};
// Usage
const MyTodos: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
if (todos.length === 0) {
return (
<EmptyState
icon="✨"
title="No todos yet"
description="Get started by adding your first todo item"
action={{
label: 'Add Todo',
onClick: () => setShowAddForm(true)
}}
/>
);
}
return <TodoList todos={todos} />;
};
Filtered Empty States
Different Empty Messages
const FilteredTodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
const [searchTerm, setSearchTerm] = useState('');
// Apply filters
const filteredTodos = todos
.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
})
.filter(todo =>
todo.text.toLowerCase().includes(searchTerm.toLowerCase())
);
// Different messages based on context
const getEmptyMessage = () => {
if (todos.length === 0) {
return 'No todos yet. Add one to get started!';
}
if (searchTerm) {
return `No todos found matching "${searchTerm}"`;
}
if (filter === 'completed') {
return 'No completed todos yet. Keep working!';
}
if (filter === 'active') {
return 'All done! No active todos.';
}
return 'No todos found.';
};
return (
<div>
{filteredTodos.length === 0 ? (
<p style={{ textAlign: 'center', color: '#666', padding: '2rem' }}>
{getEmptyMessage()}
</p>
) : (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</div>
);
};
✅ Empty State Best Practices
- Always handle empty lists - Don't show blank space
- Be contextual - Different messages for different situations
- Provide guidance - Tell users what they can do
- Add visual interest - Use icons or illustrations
- Include actions - Make it easy to add first item
⚡ List Performance
For small lists (< 100 items), performance is rarely an issue. But let's learn optimization techniques for larger lists!
Why Keys Matter for Performance
React's Reconciliation Process
When your list changes, React uses keys to:
- Determine which elements changed
- Reuse existing DOM nodes when possible
- Only update what actually changed
With good keys:
// List before: [A, B, C]
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
// Add D at position 1: [A, D, B, C]
<li key="a">A</li>
<li key="d">D</li> // ✅ New element created
<li key="b">B</li> // ✅ Reused, just moved
<li key="c">C</li> // ✅ Reused, just moved
With index as keys:
// List before: [A, B, C]
<li key="0">A</li>
<li key="1">B</li>
<li key="2">C</li>
// Add D at position 1: [A, D, B, C]
<li key="0">A</li> // ✅ Reused
<li key="1">D</li> // ⚠️ Updated (was B)
<li key="2">B</li> // ⚠️ Updated (was C)
<li key="3">C</li> // ⚠️ New element
Virtual Scrolling for Long Lists
Only Render Visible Items
For very long lists (1000+ items), use virtual scrolling libraries:
// Using react-window library
import { FixedSizeList } from 'react-window';
interface RowProps {
index: number;
style: React.CSSProperties;
}
const LargeList: React.FC = () => {
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
const Row: React.FC<RowProps> = ({ index, style }) => (
<div style={style}>
{items[index]}
</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
};
This renders only the visible items, dramatically improving performance!
Memoization with React.memo
Prevent Unnecessary Re-renders
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
// Without memo: Re-renders even if props haven't changed
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.id);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
};
// With memo: Only re-renders if props change
const TodoItem = React.memo<TodoItemProps>(({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.id);
return (
🎨 Performance Impact of Keys
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
Performance Tips
| Tip | Why It Helps |
|---|---|
| Use stable keys (IDs) | React can efficiently track items |
| Extract list items to components | Easier to optimize individual items |
| Use React.memo for items | Prevents unnecessary re-renders |
| Virtualize very long lists | Only render visible items |
| Debounce search/filter | Reduces re-renders during typing |
| Paginate or lazy load | Load data as needed |
💡 When to Optimize
Don't optimize prematurely! Consider optimization when:
- List has 500+ items
- Items are complex components
- You notice lag when typing or scrolling
- Profiling shows list is a bottleneck
For most lists under 100 items, basic React is plenty fast!
🏋️ Hands-on Practice
Time to build! These exercises will solidify your list skills.
🏋️ Exercise 1: User Directory
Goal: Build a searchable, sortable user list.
Requirements:
- Display list of users (name, email, role)
- Search by name or email
- Sort by name (A-Z, Z-A)
- Filter by role (Admin, User, Guest)
- Show empty state when no results
- Use proper keys
💡 Starter Data
interface User {
id: number;
name: string;
email: string;
role: 'Admin' | 'User' | 'Guest';
}
const users: User[] = [
{ id: 1, name: 'Alice Johnson', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Bob Smith', email: '[email protected]', role: 'User' },
{ id: 3, name: 'Charlie Brown', email: '[email protected]', role: 'Guest' },
// Add more...
];
🏋️ Exercise 2: Shopping Cart
Goal: Build a shopping cart with quantity controls.
Requirements:
- Display cart items with image, name, price, quantity
- Increase/decrease quantity buttons
- Remove item button
- Calculate and display subtotal per item
- Calculate and display total
- Empty cart state with "Start Shopping" button
💡 Hint
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
imageUrl: string;
}
const updateQuantity = (id: string, change: number) => {
setCart(cart.map(item =>
item.id === id
? { ...item, quantity: Math.max(1, item.quantity + change) }
: item
));
};
🏋️ Exercise 3: Dynamic Task Board
Goal: Build a Kanban-style task board.
Requirements:
- Three columns: "To Do", "In Progress", "Done"
- Add tasks to "To Do"
- Move tasks between columns (buttons or drag)
- Delete tasks
- Show count of tasks in each column
- Each column has its own empty state
💡 Hint
type Status = 'todo' | 'inProgress' | 'done';
interface Task {
id: number;
text: string;
status: Status;
}
const moveTask = (id: number, newStatus: Status) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, status: newStatus } : task
));
};
const todoTasks = tasks.filter(t => t.status === 'todo');
✨ Best Practices
Follow these guidelines for clean, performant lists.
✅ 1. Always Use Keys
// ✅ Good: Unique, stable key
{items.map(item => <li key={item.id}>{item.name}</li>)}
// ❌ Bad: No key
{items.map(item => <li>{item.name}</li>)}
// ⚠️ Avoid: Index as key (unless list is truly static)
{items.map((item, i) => <li key={i}>{item.name}</li>)}
✅ 2. Extract List Items to Components
// ✅ Good: Clean and reusable
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
// ❌ Bad: Complex JSX inline
{users.map(user => (
<div key={user.id}>
<img src={user.avatar} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<button>View Profile</button>
{/* 20 more lines... */}
</div>
))}
✅ 3. Handle Empty States
// ✅ Good: Helpful empty state
{items.length === 0 ? (
<EmptyState
title="No items yet"
description="Add your first item to get started"
/>
) : (
<ItemList items={items} />
)}
// ❌ Bad: Blank page
{items.map(item => <Item key={item.id} {...item} />)}
✅ 4. Type Your Lists
// ✅ Good: Fully typed
interface Todo {
id: number;
text: string;
completed: boolean;
}
const [todos, setTodos] = useState<Todo[]>([]);
// ❌ Bad: No types
const [todos, setTodos] = useState([]);
✅ 5. Use Derived State
// ✅ Good: Calculate filtered list
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState('all');
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// ❌ Bad: Store filtered list in state
const [todos, setTodos] = useState<Todo[]>([]);
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);
Lists Checklist
| Check | Done? |
|---|---|
| Every list item has a unique key | ☐ |
| Keys are stable (not random or index when list changes) | ☐ |
| Empty state is handled gracefully | ☐ |
| Complex list items extracted to components | ☐ |
| List and items are properly typed | ☐ |
| Using array methods correctly (map, filter, etc.) | ☐ |
| Filtered/sorted lists are derived, not stored | ☐ |
📚 Summary
Congratulations! You've mastered lists and keys in React - a fundamental skill for building dynamic applications.
What You Learned
📋 Rendering Lists
- Using map() to transform arrays into JSX
- Rendering arrays of objects
- Extracting list items to components
🔑 The Key Prop
- Why keys are critical for React's reconciliation
- How keys help React track elements
- Problems with using index as key
- When index keys are acceptable
🎯 Choosing Keys
- Best sources for keys (database IDs, UUIDs)
- Generating keys for new items
- Keys must be unique among siblings
- Keys must be stable and consistent
🔧 Array Methods
- map() for transforming data
- filter() for selecting items
- reduce() for aggregating values
- Chaining methods for complex operations
✏️ CRUD Operations
- Create: Adding items to lists
- Read: Displaying lists
- Update: Modifying items
- Delete: Removing items
📘 TypeScript
- Typing arrays and list components
- Type-safe array methods
- Generic list components
⚡ Performance
- How keys affect performance
- Virtual scrolling for long lists
- Memoization with React.memo
- When to optimize
🎯 Key Takeaways
- Keys are mandatory - React needs them to track list items
- Use stable IDs - Database IDs or generated UUIDs are best
- Index as key is risky - Only use for static lists
- map() is your friend - Transform data to JSX
- Handle empty states - Don't leave users confused
- Type everything - TypeScript makes lists safer
🚀 Next Steps
Now that you've mastered lists, you're ready for more advanced rendering patterns:
📖 Coming Up Next
Lesson 3.5: Conditional Rendering Patterns
- Advanced conditional rendering techniques
- Ternary operators vs logical && operators
- Switch statements in JSX
- Early returns and guard clauses
💪 Practice Projects
Build these to master lists:
- Contact Manager - CRUD for contacts with search and groups
- Expense Tracker - List of transactions with categories and totals
- Recipe Box - Recipes with ingredients lists and instructions
- Music Playlist - Songs with play counts and favorites
- Issue Tracker - Bugs/features with status, priority, and assignees
✨ Remember
Lists are everywhere in React applications. The patterns you learned here - proper keys, array methods, CRUD operations - are skills you'll use every single day as a React developer. Master these fundamentals, and you'll be able to build any kind of dynamic, data-driven interface!
📚 Additional Resources
✨ Remember
Lists are everywhere in React applications. The patterns you learned here - proper keys, array methods, CRUD operations - are skills you'll use every single day as a React developer. Master these fundamentals, and you'll be able to build any kind of dynamic, data-driven interface!
📚 Additional Resources
🎉 Congratulation 🎉 Congratulations!
You've completed Lesson 3.4 and mastered lists and keys in React! You can now render dynamic lists, choose proper keys, perform CRUD operations, and optimize list performance. This is essential knowledge for building real-world applications. Excellent work! 🚀