Lists, Keys, and Dynamic Rendering
1. Array Rendering and map() Patterns
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Basic map() | array.map(item => JSX) |
Transform array to JSX elements | Simple list rendering |
| Index parameter | array.map((item, i) => JSX) |
Access index in callback | Numbered lists, positioning |
| Array parameter | array.map((item, i, arr) => JSX) |
Access full array | Context-aware rendering |
| Inline return | {items.map(i => <div />)} |
Direct JSX return | Concise single elements |
| Block return | {items.map(i => { return <div /> })} |
Explicit return statement | Multiple operations before return |
| Fragment wrapping | map(i => <>...</>) |
Multiple elements without wrapper | Flat DOM structure |
| Destructuring | map(({id, name}) => JSX) |
Extract properties directly | Cleaner variable access |
Example: Basic list rendering patterns
// Simple list rendering
const UserList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
// With index for numbering
const NumberedList = ({ items }) => (
<ol>
{items.map((item, index) => (
<li key={item.id}>
#{index + 1}: {item.text}
</li>
))}
</ol>
);
// Destructuring in map
const ProductList = ({ products }) => (
<div>
{products.map(({ id, name, price, stock }) => (
<div key={id} className="product">
<h3>{name}</h3>
<p>${price.toFixed(2)}</p>
{stock < 10 && <span>Low stock!</span>}
</div>
))}
</div>
);
// Multiple elements with fragment
const Timeline = ({ events }) => (
<div>
{events.map(event => (
<React.Fragment key={event.id}>
<h3>{event.title}</h3>
<p>{event.description}</p>
<time>{event.date}</time>
<hr />
</React.Fragment>
))}
</div>
);
// Complex logic with block
const PostList = ({ posts }) => (
<div>
{posts.map((post) => {
const isNew = Date.now() - post.timestamp < 86400000;
const authorName = post.author.name || 'Anonymous';
return (
<article key={post.id} className={isNew ? 'new' : ''}>
<h2>{post.title}</h2>
<p>By {authorName}</p>
<p>{post.content}</p>
</article>
);
})}
</div>
);
// Nested arrays
const Categories = ({ categories }) => (
<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>
);
Note: Always return JSX from map() - common mistake is forgetting the return statement in block
syntax. Use
{} for expressions, not statements.
2. Key Prop Best Practices and Performance
| Key Type | Example | Stability | When to Use |
|---|---|---|---|
| Unique ID BEST | key={item.id} |
Stable across renders | Items from database/API |
| Compound key | key={\`\${cat}-\${id}\`} |
Stable if parts stable | Nested lists, multiple sources |
| Content hash | key={hash(item)} |
Stable for same content | Static data, computed keys |
| Index AVOID | key={index} |
Unstable on reorder | ONLY static immutable lists |
| Random/UUID NEVER | key={Math.random()} |
Never stable | Never - causes re-mount |
| Key Rule | Description | Impact if Violated |
|---|---|---|
| Unique among siblings | Keys must be unique within array | Incorrect element updates, bugs |
| Stable across renders | Same item = same key | Unnecessary re-mounts, state loss |
| Not globally unique | Only unique in local array | No issue - scoped to parent |
| No array index for dynamic | Avoid index if items move/delete | Wrong items update, state mismatch |
| Don't use objects | Keys must be string/number | Warning, uses toString() |
Example: Key prop patterns and anti-patterns
// ✅ GOOD: Stable unique IDs from data
const GoodList = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
// ✅ GOOD: Compound keys for nested structures
const NestedGood = ({ categories }) => (
<div>
{categories.map(cat => (
<div key={cat.id}>
<h2>{cat.name}</h2>
{cat.products.map(prod => (
<div key={\`\${cat.id}-\${prod.id}\`}>
{prod.name}
</div>
))}
</div>
))}
</div>
);
// ⚠️ ACCEPTABLE: Index only for static lists
const StaticList = () => {
const staticItems = ['Home', 'About', 'Contact']; // Never changes
return (
<ul>
{staticItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
};
// ❌ BAD: Index with dynamic operations
const BadDynamic = ({ items, onDelete }) => (
<ul>
{items.map((item, index) => (
<li key={index}> {/* BUG: Keys shift on delete */}
<input defaultValue={item.name} />
<button onClick={() => onDelete(index)}>Delete</button>
</li>
))}
</ul>
);
// ❌ BAD: Random/changing keys
const BadRandom = ({ items }) => (
<ul>
{items.map(item => (
<li key={Math.random()}> {/* Always re-mounts */}
{item.name}
</li>
))}
</ul>
);
// ❌ BAD: Non-unique keys
const BadDuplicate = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.type}> {/* Multiple items same type */}
{item.name}
</li>
))}
</ul>
);
// ✅ GOOD: Generate stable ID if missing
const WithGeneratedID = ({ items }) => {
const itemsWithIds = useMemo(() =>
items.map((item, index) => ({
...item,
_key: item.id || \`generated-\${index}-\${item.name}\`
})),
[items]
);
return (
<ul>
{itemsWithIds.map(item => (
<li key={item._key}>{item.name}</li>
))}
</ul>
);
};
Warning: Using array index as key with reorderable/deletable lists causes bugs:
component state (like input values) gets assigned to wrong items after operations.
3. Dynamic List Updates and State Management
| Operation | Pattern | Immutability |
|---|---|---|
| Add to end | [...items, newItem] |
Creates new array |
| Add to start | [newItem, ...items] |
Creates new array |
| Insert at index | [...items.slice(0, i), item, ...items.slice(i)] |
New array with insertion |
| Remove by index | items.filter((_, i) => i !== index) |
New array without item |
| Remove by ID | items.filter(i => i.id !== id) |
New array without item |
| Update by index | items.map((i, idx) => idx === index ? {...i, prop} : i) |
New array with updated item |
| Update by ID | items.map(i => i.id === id ? {...i, prop} : i) |
New array with updated item |
| Replace all | setItems(newItems) |
Complete replacement |
| Sort | [...items].sort(compareFn) |
Copy then sort |
| Reverse | [...items].reverse() |
Copy then reverse |
Example: Dynamic list operations with immutable state
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// Add new todo
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
}
};
// Delete todo by ID
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// Toggle completed status
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// Update todo text
const updateTodo = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
));
};
// Move todo up
const moveUp = (index) => {
if (index > 0) {
const newTodos = [...todos];
[newTodos[index - 1], newTodos[index]] =
[newTodos[index], newTodos[index - 1]];
setTodos(newTodos);
}
};
// Clear completed
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<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={() => moveUp(index)}>↑</button>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
<button onClick={clearCompleted}>Clear Completed</button>
</div>
);
};
// Advanced: Batch updates with useReducer
const listReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, action.item];
case 'DELETE':
return state.filter(item => item.id !== action.id);
case 'UPDATE':
return state.map(item =>
item.id === action.id
? { ...item, ...action.updates }
: item
);
case 'REORDER':
const result = [...state];
const [removed] = result.splice(action.from, 1);
result.splice(action.to, 0, removed);
return result;
case 'BULK_UPDATE':
return state.map(item =>
action.ids.includes(item.id)
? { ...item, ...action.updates }
: item
);
default:
return state;
}
};
const AdvancedList = () => {
const [items, dispatch] = useReducer(listReducer, []);
return (
<div>
<button onClick={() => dispatch({
type: 'ADD',
item: { id: Date.now(), text: 'New item' }
})}>
Add Item
</button>
{items.map(item => (
<div key={item.id}>
{item.text}
<button onClick={() => dispatch({
type: 'DELETE',
id: item.id
})}>
Delete
</button>
</div>
))}
</div>
);
};
Note: Never mutate state arrays directly (
items.push(),
items[0] = x).
Always create new arrays to trigger React re-renders properly.
4. Conditional Rendering and Null Patterns
| Pattern | Syntax | Renders | Use Case |
|---|---|---|---|
| Logical AND | {condition && <Component />} |
Component or nothing | Simple show/hide |
| Ternary | {condition ? <A /> : <B />} |
Either A or B | Two alternatives |
| Null coalescing | {value ?? 'default'} |
Value or default | Null/undefined fallback |
| Optional chaining | {obj?.prop?.value} |
Value or undefined | Safe nested access |
| Array filter | {arr.filter(cond).map(...)} |
Filtered items | Conditional lists |
| Early return | if (!data) return null; |
Nothing or rest | Loading/error states |
| Empty fragment | <>{condition && ...}</> |
Conditional children | Multiple conditional elements |
| Nullish rendering | {value || 'N/A'} |
Value or fallback | Display defaults |
Example: Conditional rendering patterns
// Logical AND for simple conditions
const Greeting = ({ isLoggedIn, username }) => (
<div>
<h1>Welcome</h1>
{isLoggedIn && <p>Hello, {username}!</p>}
</div>
);
// Ternary for either/or
const Status = ({ isOnline }) => (
<div>
{isOnline ? (
<span className="online">Online</span>
) : (
<span className="offline">Offline</span>
)}
</div>
);
// Early returns for complex conditions
const UserProfile = ({ user, isLoading, error }) => {
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
// Nested conditionals with optional chaining
const Address = ({ user }) => (
<div>
<p>{user?.address?.street ?? 'No address provided'}</p>
<p>{user?.address?.city}, {user?.address?.country}</p>
</div>
);
// Multiple conditions in list
const ItemList = ({ items, showOnlyActive, sortByName }) => {
const filteredItems = items
.filter(item => !showOnlyActive || item.active)
.sort((a, b) => sortByName ? a.name.localeCompare(b.name) : 0);
return (
<ul>
{filteredItems.length === 0 ? (
<li>No items to display</li>
) : (
filteredItems.map(item => (
<li key={item.id}>
{item.name}
{item.isNew && <span className="badge">NEW</span>}
</li>
))
)}
</ul>
);
};
// Complex multi-condition rendering
const Dashboard = ({ user, permissions, settings }) => (
<div>
{/* Admin section */}
{user.role === 'admin' && (
<section>
<h2>Admin Panel</h2>
{permissions.canEdit && <button>Edit</button>}
{permissions.canDelete && <button>Delete</button>}
</section>
)}
{/* Premium features */}
{user.isPremium ? (
<PremiumFeatures />
) : (
<div>
<p>Upgrade to Premium</p>
<button>Upgrade Now</button>
</div>
)}
{/* Settings based on flags */}
{settings.showNotifications && <NotificationPanel />}
{settings.enableDarkMode && <ThemeToggle />}
</div>
);
// Avoiding falsy value bugs
const Counter = ({ count }) => (
<div>
{/* ❌ BAD: renders "0" when count is 0 */}
{count && <p>Count: {count}</p>}
{/* ✅ GOOD: explicit check */}
{count > 0 && <p>Count: {count}</p>}
{/* ✅ GOOD: ternary for all cases */}
{count ? <p>Count: {count}</p> : <p>No items</p>}
</div>
);
Warning: Watch out for falsy values in logical AND:
0 && <Component />
renders "0", not nothing. Use explicit boolean checks or ternary operators.
5. List Filtering and Search Implementation
| Technique | Method | Performance | Use Case |
|---|---|---|---|
| Simple filter | items.filter(predicate) |
O(n) | Small lists, simple conditions |
| Case-insensitive search | toLowerCase().includes() |
O(n×m) | Text search |
| Multiple criteria | filter(i => cond1 && cond2) |
O(n) | Advanced filtering |
| Debounced search | Delay filter execution | Reduced calls | Live search, API calls |
| Memoized filter | useMemo(() => filter) |
Cached result | Expensive filters, large lists |
| Index search | Pre-built lookup maps | O(1) lookup | Very large datasets |
Example: Search and filter implementations
// Basic search filter
const SearchList = () => {
const [items] = useState([
{ id: 1, name: 'Apple', category: 'Fruit' },
{ id: 2, name: 'Banana', category: 'Fruit' },
{ id: 3, name: 'Carrot', category: 'Vegetable' }
]);
const [search, setSearch] = useState('');
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
// Multi-criteria filter with memoization
const AdvancedFilter = () => {
const [products, setProducts] = useState([]);
const [filters, setFilters] = useState({
search: '',
category: 'all',
minPrice: 0,
maxPrice: 1000,
inStock: false
});
const filteredProducts = useMemo(() => {
return products.filter(product => {
// Search filter
if (filters.search &&
!product.name.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
// Category filter
if (filters.category !== 'all' && product.category !== filters.category) {
return false;
}
// Price range
if (product.price < filters.minPrice || product.price > filters.maxPrice) {
return false;
}
// Stock filter
if (filters.inStock && product.stock === 0) {
return false;
}
return true;
});
}, [products, filters]);
return (
<div>
<input
value={filters.search}
onChange={e => setFilters({...filters, search: e.target.value})}
placeholder="Search products..."
/>
<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>
<p>Found {filteredProducts.length} products</p>
<div>
{filteredProducts.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
<p>Stock: {product.stock}</p>
</div>
))}
</div>
</div>
);
};
// Debounced search for better performance
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
const DebouncedSearch = () => {
const [items] = useState([/* large array */]);
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
const filteredItems = useMemo(() => {
if (!debouncedSearch) return items;
return items.filter(item =>
item.name.toLowerCase().includes(debouncedSearch.toLowerCase())
);
}, [items, debouncedSearch]);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search (debounced)..."
/>
<p>Searching for: {debouncedSearch}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
// Sorting combined with filtering
const SortableFilterable = () => {
const [items, setItems] = useState([]);
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('name');
const [sortOrder, setSortOrder] = useState('asc');
const processedItems = useMemo(() => {
// First filter
let result = items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
// Then sort
result.sort((a, b) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
if (typeof aVal === 'string') {
return sortOrder === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
}
return sortOrder === 'asc'
? aVal - bVal
: bVal - aVal;
});
return result;
}, [items, search, sortBy, sortOrder]);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
/>
<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>
<ul>
{processedItems.map(item => (
<li key={item.id}>{item.name} - ${item.price}</li>
))}
</ul>
</div>
);
};
Note: Use
useMemo for expensive filter/sort operations on large lists.
Add debouncing for search inputs to reduce unnecessary re-renders.
6. Virtualized Lists and Large Dataset Handling
| Technique | Description | Best For | Library |
|---|---|---|---|
| Windowing | Render only visible items | Long uniform lists | react-window |
| Virtual scrolling | Dynamic item height handling | Variable height items | react-virtualized |
| Infinite scroll | Load more on scroll bottom | Paginated data | react-infinite-scroll |
| Pagination | Page-based navigation | Discrete data chunks | Custom implementation |
| Lazy rendering | Render on intersection | Complex card layouts | IntersectionObserver |
| Cursor-based | Load by cursor position | Real-time feeds | API + state management |
Example: Virtualized list with react-window
import { FixedSizeList } from 'react-window';
// Basic virtualized list
const VirtualList = ({ 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>
);
};
// Variable height list
import { VariableSizeList } from 'react-window';
const VariableHeightList = ({ items }) => {
const listRef = useRef();
const getItemSize = (index) => {
// Calculate height based on content
return items[index].description.length > 100 ? 120 : 60;
};
const Row = ({ index, style }) => (
<div style={style}>
<h4>{items[index].title}</h4>
<p>{items[index].description}</p>
</div>
);
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
};
// Infinite scroll implementation
const InfiniteScrollList = () => {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerRef = useRef();
const lastItemRef = useCallback((node) => {
if (loading) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setPage(prev => prev + 1);
}
});
if (node) observerRef.current.observe(node);
}, [loading, hasMore]);
useEffect(() => {
const fetchMore = async () => {
setLoading(true);
const response = await fetch(\`/api/items?page=\${page}\`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setLoading(false);
};
fetchMore();
}, [page]);
return (
<div>
{items.map((item, index) => {
if (items.length === index + 1) {
return (
<div ref={lastItemRef} key={item.id}>
{item.name}
</div>
);
}
return <div key={item.id}>{item.name}</div>;
})}
{loading && <div>Loading...</div>}
</div>
);
};
// Pagination implementation
const PaginatedList = () => {
const [items, setItems] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const itemsPerPage = 20;
useEffect(() => {
const fetchPage = async () => {
const response = await fetch(
\`/api/items?page=\${currentPage}&limit=\${itemsPerPage}\`
);
const data = await response.json();
setItems(data.items);
setTotalPages(Math.ceil(data.total / itemsPerPage));
};
fetchPage();
}, [currentPage]);
return (
<div>
<ul>
{items.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>
);
};
// Lazy loading with intersection observer
const LazyList = ({ items }) => {
const [visibleItems, setVisibleItems] = useState(new Set());
const observeItem = useCallback((node, id) => {
if (!node) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setVisibleItems(prev => new Set([...prev, id]));
observer.disconnect();
}
});
},
{ rootMargin: '100px' }
);
observer.observe(node);
}, []);
return (
<div>
{items.map(item => (
<div
key={item.id}
ref={node => observeItem(node, item.id)}
style={{ minHeight: '100px' }}
>
{visibleItems.has(item.id) ? (
<ExpensiveComponent item={item} />
) : (
<div>Loading...</div>
)}
</div>
))}
</div>
);
};
Performance Tips:
- Use virtualization for lists > 100 items
- Implement infinite scroll for continuous feeds
- Prefer pagination for searchable/filterable data
- Use IntersectionObserver for lazy loading images/components
- Memoize row components with React.memo