Conditional Rendering and Lists
Learn how to conditionally render content and render lists of items in React.
Topics Covered:
Prerequisites:
- Understanding Props
Overview
Conditional rendering and lists are two of the most fundamental patterns in React. Almost every component you build will use these patterns. Conditional rendering lets you show different content based on state or props, while list rendering allows you to display collections of data efficiently. This tutorial covers all the ways to conditionally render content, how to render lists properly, the importance of keys, and common patterns you'll use in real applications.
Lesson 1: Understanding Conditional Rendering
Conditional rendering means showing different UI based on conditions. React provides several ways to do this, each with its own use case. Why Conditional Rendering? • Show/hide elements based on state • Display different content for different users • Handle loading and error states • Create dynamic, interactive UIs Common Use Cases: • User authentication states • Loading indicators • Error messages • Feature flags • Empty states • Form validation messages
// Basic conditional rendering examples
// Example 1: User authentication
function App() {
const [user, setUser] = useState(null);
// Different content for logged in vs logged out
if (user) {
return <Dashboard user={user} />;
}
return <LoginForm onLogin={setUser} />;
}
// Example 2: Loading state
function DataDisplay() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
if (loading) {
return <div>Loading...</div>;
}
if (!data) {
return <div>No data available</div>;
}
return <div>{data.content}</div>;
}
// Example 3: Feature flags
function Feature({ enabled }: { enabled: boolean }) {
if (!enabled) {
return null; // Don't render anything
}
return <div>New Feature Content</div>;
}Conditional rendering is essential for dynamic UIs. Use it to show different content based on state, user permissions, loading states, and more.
Lesson 2: Conditional Rendering Methods
React provides three main ways to conditionally render content. Each has its place. Method 1: If Statements (Early Returns) • Best for: Completely different components • Use when: You want to return different JSX entirely • Clean and readable • Can't use inside JSX Method 2: Ternary Operator (? :) • Best for: Two options • Use when: You want to choose between two elements • Can be used inside JSX • More concise than if/else Method 3: Logical AND (&&) • Best for: Show/hide single element • Use when: You want to show something or nothing • Can be used inside JSX • Beware of falsy values!
// Method 1: If statements (early returns)
function UserProfile({ user, isAdmin }) {
// Early return for different cases
if (!user) {
return <div>Please log in</div>;
}
if (isAdmin) {
return <AdminDashboard user={user} />;
}
return <RegularDashboard user={user} />;
}
// Method 2: Ternary operator (two options)
function Greeting({ isLoggedIn, userName }) {
return (
<div>
{isLoggedIn ? (
<h1>Welcome back, {userName}!</h1>
) : (
<h1>Please log in to continue</h1>
)}
</div>
);
}
// Ternary for inline values
function PriceDisplay({ price, discount }: { price: number; discount: boolean }) {
return (
<div>
Price: ${discount ? price * 0.9 : price}
{discount && <span className="badge">On Sale!</span>}
</div>
);
}
// Method 3: Logical AND (show/hide)
function NotificationBanner({ message }) {
return (
<div>
{message && (
<div className="banner">
{message}
</div>
)}
</div>
);
}
// ⚠️ Common mistake with &&
function Counter({ count }) {
// ❌ BAD: If count is 0, nothing renders!
return <div>{count && <span>Count: {count}</span>}</div>;
// ✅ GOOD: Explicit check
return <div>{count > 0 && <span>Count: {count}</span>}</div>;
// ✅ BETTER: Use ternary for clarity
return <div>{count > 0 ? <span>Count: {count}</span> : null}</div>;
}
// Complex conditional rendering
function StatusDisplay({ status, data, error }) {
// Multiple conditions
if (error) {
return <ErrorMessage error={error} />;
}
if (!data) {
return <LoadingSpinner />;
}
if (data.length === 0) {
return <EmptyState />;
}
return <DataList data={data} />;
}
// Nested conditionals
function ProductCard({ product, user }) {
return (
<div>
<h3>{product.name}</h3>
<p>{product.description}</p>
{/* Multiple conditions */}
{product.onSale && (
<span className="sale-badge">Sale!</span>
)}
{user?.isPremium ? (
<PremiumPrice price={product.price} />
) : (
<RegularPrice price={product.price} />
)}
{user && (
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
)}
</div>
);
}Each conditional rendering method has its place. Use if statements for early returns, ternary for two options, and && for show/hide. Be careful with && and falsy values like 0.
Lesson 3: Common Conditional Rendering Patterns
Learn common patterns you'll use in real applications. Common Patterns: • Loading states • Error states • Empty states • Authentication states • Permission-based rendering • Feature flags • Form validation Best Practices: • Keep conditions clear and readable • Extract complex conditionals to variables • Use early returns for clarity • Avoid deeply nested ternaries
// Pattern 1: Loading, Error, Success states
function DataFetcher() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
// Early returns for different states
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return <EmptyState />;
return <DataDisplay data={data} />;
}
// Pattern 2: Permission-based rendering
function AdminPanel({ user }) {
const isAdmin = user?.role === 'admin';
const canEdit = user?.permissions?.includes('edit');
return (
<div>
<h1>Admin Panel</h1>
{isAdmin && (
<div>
<AdminControls />
{canEdit && <EditButton />}
</div>
)}
{!isAdmin && (
<div>You don't have permission to view this</div>
)}
</div>
);
}
// Pattern 3: Feature flags
function App() {
const features = {
newDashboard: true,
darkMode: false,
betaFeatures: true,
};
return (
<div>
<Header />
{features.newDashboard ? (
<NewDashboard />
) : (
<OldDashboard />
)}
{features.darkMode && <DarkModeToggle />}
</div>
);
}
// Pattern 4: Form validation messages
function FormField({ value, error, touched }) {
return (
<div>
<input value={value} />
{touched && error && (
<span className="error">{error}</span>
)}
{touched && !error && value && (
<span className="success">✓</span>
)}
</div>
);
}
// Pattern 5: Conditional classes
function Button({ variant, disabled, children }) {
const className = [
'btn',
variant && `btn-${variant}`,
disabled && 'btn-disabled',
]
.filter(Boolean)
.join(' ');
return (
<button className={className} disabled={disabled}>
{children}
</button>
);
}
// Pattern 6: Extract complex conditions
function ProductList({ products, user }) {
// Extract complex condition to variable
const hasDiscount = products.some(p => p.onSale);
const canViewPremium = user?.subscription === 'premium';
const showPremiumProducts = canViewPremium && hasDiscount;
return (
<div>
{showPremiumProducts && (
<PremiumProductsBanner />
)}
{products.map(product => (
<ProductCard
key={product.id}
product={product}
showDiscount={hasDiscount}
/>
))}
</div>
);
}
// Pattern 7: Multiple conditions with helper
function getStatusMessage(status, data) {
if (status === 'loading') return 'Loading...';
if (status === 'error') return 'Something went wrong';
if (!data || data.length === 0) return 'No items found';
return null;
}
function StatusDisplay({ status, data }) {
const message = getStatusMessage(status, data);
if (message) {
return <div className="status">{message}</div>;
}
return <DataList data={data} />;
}Common patterns include loading/error/success states, permission checks, feature flags, and form validation. Extract complex conditions to variables for readability.
Lesson 4: Rendering Lists with map()
Rendering lists is one of the most common tasks in React. The map() method is your primary tool. Why map()? • Transforms array into JSX elements • Returns new array (doesn't mutate original) • Works with any array length • Can be combined with other array methods Basic Pattern: ```jsx {array.map(item => <Component key={item.id} item={item} />)} ``` Important Rules: • Always provide a key prop • Keys must be unique • Keys should be stable • Don't use array index as key (if items can reorder)
// Basic list rendering
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// With component extraction
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
function TodoItem({ todo }) {
return (
<li>
<input type="checkbox" checked={todo.completed} />
<span>{todo.text}</span>
</li>
);
}
// Rendering with index (when items don't reorder)
function StaticList({ items }) {
// Only use index if list is static and won't reorder
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
// Rendering nested data
function UserList({ users }) {
return (
<div>
{users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<ul>
{user.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
))}
</div>
);
}
// Conditional rendering within lists
function ProductList({ products, showPrices }) {
return (
<div>
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
{showPrices && <p>${product.price}</p>}
{product.onSale && <span>On Sale!</span>}
</div>
))}
</div>
);
}
// Empty list handling
function TodoList({ todos }) {
if (todos.length === 0) {
return <div>No todos yet. Add one to get started!</div>;
}
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// Filtered lists
function CompletedTodos({ todos }) {
const completed = todos.filter(todo => todo.completed);
return (
<ul>
{completed.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// Sorted lists
function SortedProducts({ products }) {
const sorted = [...products].sort((a, b) => a.price - b.price);
return (
<div>
{sorted.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Use map() to transform arrays into JSX. Always provide unique, stable keys. Handle empty lists gracefully. Combine with filter, sort, and other array methods.
Lesson 5: Understanding Keys in Lists
Keys are crucial for React's reconciliation algorithm. Understanding them prevents bugs and improves performance. What are Keys? • Special prop that helps React identify items • Must be unique among siblings • Should be stable across renders • Not accessible in component (not a prop) Why Keys Matter: • Help React identify which items changed • Enable efficient updates • Prevent unnecessary re-renders • Maintain component state correctly Key Rules: • Must be unique among siblings • Should be stable (don't change) • Should be predictable (not random) • Use item IDs when available • Only use index if list is static
// ✅ GOOD: Using unique IDs
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// ❌ BAD: Using index when items can reorder
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
// Problem: If todos reorder, React thinks items changed
// Component state gets mixed up!
))}
</ul>
);
}
// ✅ GOOD: Using index only for static lists
function StaticMenu({ items }) {
// OK because menu items never reorder
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
// ❌ BAD: No key (React warning)
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem todo={todo} /> {/* Missing key! */}
))}
</ul>
);
}
// ✅ GOOD: Creating keys from multiple fields
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<UserItem
key={`${user.id}-${user.email}`}
user={user}
/>
))}
</ul>
);
}
// ✅ GOOD: Using stable IDs from API
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<ProductCard
key={product.sku} // Stable, unique identifier
product={product}
/>
))}
</div>
);
}
// ❌ BAD: Using random keys
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={Math.random()} // ❌ New key every render!
todo={todo}
/>
))}
</ul>
);
}
// What happens without proper keys:
// Before: [A(id:1), B(id:2), C(id:3)]
// After: [C(id:3), A(id:1), B(id:2)]
// With proper keys:
// React knows C moved from position 2 to 0
// React reuses components, just moves them
// State is preserved correctly
// Without proper keys (using index):
// React thinks item at 0 changed from A to C
// React destroys A and creates new C
// State is lost!
// Example: Input state gets mixed up
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoInput key={index} todo={todo} />
// If todos reorder, input values get mixed up!
))}
</ul>
);
}
// Fix: Use stable IDs
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoInput key={todo.id} todo={todo} />
// Now input state is preserved correctly
))}
</ul>
);
}Keys are essential for React's reconciliation. Use stable, unique identifiers (like IDs) when items can reorder. Only use index for static lists. Bad keys cause bugs and performance issues.
Lesson 6: Advanced List Patterns
Learn advanced patterns for rendering lists in real applications. Advanced Patterns: • Filtered lists • Sorted lists • Grouped lists • Paginated lists • Virtual scrolling • Nested lists • Lists with actions Best Practices: • Extract list items to components • Handle empty states • Optimize with React.memo for large lists • Use keys properly • Consider virtualization for very long lists
// Pattern 1: Filtered and sorted lists
function ProductList({ products, category, sortBy }) {
const filtered = products.filter(
product => product.category === category
);
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
return (
<div>
{sorted.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Pattern 2: Grouped lists
function GroupedList({ items }) {
const grouped = items.reduce((groups, item) => {
const category = item.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(item);
return groups;
}, {});
return (
<div>
{Object.entries(grouped).map(([category, items]) => (
<div key={category}>
<h2>{category}</h2>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
// Pattern 3: Lists with actions
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)}
onDelete={() => onDelete(todo.id)}
/>
))}
</ul>
);
}
// Pattern 4: Conditional list items
function NotificationList({ notifications }) {
return (
<ul>
{notifications.map(notification => (
<li key={notification.id}>
{notification.type === 'error' && (
<ErrorIcon />
)}
{notification.type === 'success' && (
<SuccessIcon />
)}
<span>{notification.message}</span>
{notification.actionable && (
<button>Action</button>
)}
</li>
))}
</ul>
);
}
// Pattern 5: Lists with loading states
function DataList({ data, loading, error }) {
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
if (data.length === 0) {
return <div>No items found</div>;
}
return (
<ul>
{data.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
}
// Pattern 6: Memoized list items (performance)
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
<span>{todo.text}</span>
</li>
);
});
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)}
/>
))}
</ul>
);
}
// Pattern 7: Lists with pagination
function PaginatedList({ items, page, pageSize }) {
const startIndex = page * pageSize;
const endIndex = startIndex + pageSize;
const pageItems = items.slice(startIndex, endIndex);
return (
<div>
<ul>
{pageItems.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
<Pagination
currentPage={page}
totalPages={Math.ceil(items.length / pageSize)}
/>
</div>
);
}
// Pattern 8: Nested lists
function CategoryList({ categories }) {
return (
<ul>
{categories.map(category => (
<li key={category.id}>
<h3>{category.name}</h3>
<ul>
{category.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</li>
))}
</ul>
);
}Advanced list patterns include filtering, sorting, grouping, pagination, and conditional rendering. Extract list items to components and use React.memo for performance with large lists.
Lesson 7: Common Mistakes and How to Fix Them
Avoid common mistakes when rendering lists and conditional content. Common Mistakes: • Missing keys • Using index as key when items reorder • Mutating arrays directly • Not handling empty lists • Falsy values with && • Deeply nested ternaries • Forgetting to handle loading/error states
// ❌ MISTAKE 1: Missing keys
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li>{todo.text}</li> // Missing key!
))}
</ul>
);
}
// ✅ FIX: Add unique key
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// ❌ MISTAKE 2: Using index when items reorder
function SortableList({ items }) {
const [sorted, setSorted] = useState(items);
return (
<ul>
{sorted.map((item, index) => (
<li key={index}>{item.name}</li>
// Bug: State gets mixed up when sorted!
))}
</ul>
);
}
// ✅ FIX: Use stable IDs
function SortableList({ items }) {
const [sorted, setSorted] = useState(items);
return (
<ul>
{sorted.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// ❌ MISTAKE 3: Mutating arrays
function TodoList({ todos, onAdd }) {
todos.push({ id: 1, text: 'New' }); // Mutates original!
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// ✅ FIX: Create new array
function TodoList({ todos, onAdd }) {
const newTodos = [...todos, { id: 1, text: 'New' }];
return (
<ul>
{newTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// ❌ MISTAKE 4: Falsy values with &&
function Counter({ count }) {
return <div>{count && <span>Count: {count}</span>}</div>;
// If count is 0, nothing renders!
}
// ✅ FIX: Explicit check
function Counter({ count }) {
return (
<div>
{count !== null && count !== undefined && (
<span>Count: {count}</span>
)}
</div>
);
// Or use ternary
return <div>{count != null ? <span>Count: {count}</span> : null}</div>;
}
// ❌ MISTAKE 5: Not handling empty lists
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
// If products is empty, shows nothing (confusing)
}
// ✅ FIX: Handle empty state
function ProductList({ products }) {
if (products.length === 0) {
return <div>No products found</div>;
}
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// ❌ MISTAKE 6: Deeply nested ternaries
function StatusDisplay({ status, data, error }) {
return (
<div>
{status === 'loading' ? (
<Loading />
) : status === 'error' ? (
<Error error={error} />
) : data ? (
<Data data={data} />
) : (
<Empty />
)}
</div>
);
// Hard to read!
}
// ✅ FIX: Early returns or extract logic
function StatusDisplay({ status, data, error }) {
if (status === 'loading') return <Loading />;
if (status === 'error') return <Error error={error} />;
if (!data) return <Empty />;
return <Data data={data} />;
}
// ❌ MISTAKE 7: Forgetting loading/error states
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user.name}</div>; // Crashes if user is null!
}
// ✅ FIX: Handle all states
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Loading />;
if (error) return <Error error={error} />;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}Common mistakes include missing keys, using index for reorderable lists, mutating arrays, not handling empty states, falsy value issues with &&, and forgetting loading/error states. Always handle all possible states.
Lesson 8: Performance Considerations
Rendering large lists can impact performance. Learn optimization techniques. Performance Tips: • Use React.memo for list items • Virtual scrolling for very long lists • Avoid inline functions in map • Use stable keys • Consider pagination • Lazy load items When to Optimize: • Lists with 100+ items • Complex list items • Frequent re-renders • Slow interactions
// Optimize with React.memo
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={onToggle}
/>
<span>{todo.text}</span>
</li>
);
});
// ❌ BAD: Inline function creates new function every render
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)} // New function each render
/>
))}
</ul>
);
}
// ✅ GOOD: Use useCallback
function TodoList({ todos, onToggle }) {
const handleToggle = useCallback((id) => {
onToggle(id);
}, [onToggle]);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</ul>
);
}
// ✅ BETTER: Pass ID and handler separately
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
id={todo.id}
onToggle={onToggle} // Stable reference
/>
))}
</ul>
);
}
// Virtual scrolling for very long lists
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Pagination for large datasets
function PaginatedList({ items }) {
const [page, setPage] = useState(0);
const pageSize = 20;
const startIndex = page * pageSize;
const pageItems = items.slice(startIndex, startIndex + pageSize);
return (
<div>
<ul>
{pageItems.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
<Pagination
currentPage={page}
totalPages={Math.ceil(items.length / pageSize)}
onPageChange={setPage}
/>
</div>
);
}Optimize large lists with React.memo, useCallback, virtual scrolling, and pagination. Avoid inline functions in map() that create new functions on every render.
Conclusion
Conditional rendering and lists are fundamental React patterns you'll use in every application. Master the different conditional rendering methods, always use proper keys for lists, handle all states (loading, error, empty), and optimize for performance when needed. These patterns form the foundation of dynamic React applications.