Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props
2.2.c — Dynamic Rendering
In one sentence: Dynamic rendering is the practice of changing what a component displays based on the data it receives through props, making UIs responsive to data without hardcoding content.
Navigation: ← 2.2.b — Understanding Props · Next → 2.2.d — Rendering Lists
Table of Contents
- What Is Dynamic Rendering?
- Dynamic Text and Numbers
- Dynamic Images and Media
- Dynamic Styles
- Dynamic CSS Classes
- Conditional Rendering Basics
- Rendering Based on Data Shape
- Component Variants with Props
- Status Indicators and Badges
- Role-Based and Permission-Based UI
- Dynamic Component Selection
- Template Patterns for Real UIs
- Performance Considerations
- Common Mistakes
- Key Takeaways
- Explain-It Challenge
1. What Is Dynamic Rendering?
Static rendering means every user sees the exact same thing. Dynamic rendering means the UI adapts based on data.
┌──────────────────────────────────────────────────────────┐
│ STATIC vs DYNAMIC RENDERING │
│ │
│ STATIC (hardcoded): │
│ ┌───────────────────────┐ │
│ │ Hello, World! │ ← Same for everyone │
│ │ You have 0 messages │ ← Never changes │
│ │ [ Default Avatar ] │ ← Fixed image │
│ └───────────────────────┘ │
│ │
│ DYNAMIC (data-driven): │
│ ┌───────────────────────┐ │
│ │ Hello, Alice! │ ← From props.name │
│ │ You have 5 messages │ ← From props.count │
│ │ [ Alice's Avatar ] │ ← From props.avatar │
│ │ 🟢 Online │ ← From props.status │
│ │ [Admin Badge] │ ← From props.role │
│ └───────────────────────┘ │
│ │
│ Same component, completely different output. │
└──────────────────────────────────────────────────────────┘
Everything in React that uses {} inside JSX is dynamic rendering:
// Every {} is a dynamic insertion point
function WelcomeCard({ name, role, messageCount, lastLogin }) {
return (
<div className="welcome-card">
<h1>Welcome back, {name}!</h1>
<p>Role: {role}</p>
<p>You have {messageCount} unread messages</p>
<p>Last login: {lastLogin}</p>
</div>
);
}
2. Dynamic Text and Numbers
Basic Text Interpolation
function UserGreeting({ firstName, lastName, title }) {
return (
<div>
{/* Simple interpolation */}
<h1>Hello, {firstName}!</h1>
{/* Combining multiple props */}
<h2>{title} {firstName} {lastName}</h2>
{/* Template literal for complex strings */}
<p>{`Welcome to your dashboard, ${firstName} ${lastName}`}</p>
{/* Expressions */}
<p>Initials: {firstName[0]}{lastName[0]}</p>
{/* Method calls */}
<p>Username: {firstName.toLowerCase()}.{lastName.toLowerCase()}</p>
</div>
);
}
<UserGreeting firstName="Alice" lastName="Johnson" title="Dr." />
// Renders:
// Hello, Alice!
// Dr. Alice Johnson
// Welcome to your dashboard, Alice Johnson
// Initials: AJ
// Username: alice.johnson
Number Formatting
function PriceDisplay({ amount, currency = "USD", locale = "en-US" }) {
const formatted = new Intl.NumberFormat(locale, {
style: "currency",
currency: currency,
}).format(amount);
return <span className="price">{formatted}</span>;
}
function StatsCard({ label, value, unit, decimals = 0 }) {
// Format large numbers with commas
const formattedValue = typeof value === "number"
? value.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
: value;
return (
<div className="stat-card">
<p className="stat-label">{label}</p>
<p className="stat-value">
{formattedValue}
{unit && <span className="stat-unit"> {unit}</span>}
</p>
</div>
);
}
// Usage
<PriceDisplay amount={1299.99} currency="USD" /> // $1,299.99
<PriceDisplay amount={1299.99} currency="EUR" locale="de-DE" /> // 1.299,99 €
<StatsCard label="Revenue" value={1500000} unit="USD" /> // 1,500,000 USD
<StatsCard label="Conversion" value={3.456} unit="%" decimals={2} /> // 3.46 %
Date Formatting
function DateDisplay({ date, format = "medium" }) {
const d = new Date(date);
const formats = {
short: { month: "numeric", day: "numeric", year: "2-digit" },
medium: { month: "short", day: "numeric", year: "numeric" },
long: { month: "long", day: "numeric", year: "numeric", weekday: "long" },
relative: null, // handled separately
};
if (format === "relative") {
return <time dateTime={d.toISOString()}>{getRelativeTime(d)}</time>;
}
const formatted = d.toLocaleDateString("en-US", formats[format]);
return <time dateTime={d.toISOString()}>{formatted}</time>;
}
function getRelativeTime(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
// Usage
<DateDisplay date="2026-04-11" format="short" /> // 4/11/26
<DateDisplay date="2026-04-11" format="medium" /> // Apr 11, 2026
<DateDisplay date="2026-04-11" format="long" /> // Friday, April 11, 2026
<DateDisplay date={Date.now() - 300000} format="relative" /> // 5m ago
3. Dynamic Images and Media
function Avatar({
src,
name,
size = 48,
status,
fallback = "/images/default-avatar.png"
}) {
const [imgError, setImgError] = useState(false);
const statusColors = {
online: "#4caf50",
offline: "#9e9e9e",
busy: "#f44336",
away: "#ff9800",
};
return (
<div style={{ position: "relative", width: size, height: size }}>
<img
src={imgError ? fallback : src}
alt={`${name}'s avatar`}
width={size}
height={size}
style={{ borderRadius: "50%", objectFit: "cover" }}
onError={() => setImgError(true)}
/>
{status && (
<span
style={{
position: "absolute",
bottom: 0,
right: 0,
width: size * 0.25,
height: size * 0.25,
borderRadius: "50%",
backgroundColor: statusColors[status] || "#9e9e9e",
border: "2px solid white",
}}
title={status}
/>
)}
</div>
);
}
// Usage
<Avatar src="/alice.jpg" name="Alice" size={64} status="online" />
<Avatar src="/bob.jpg" name="Bob" size={32} status="busy" />
<Avatar src="/broken-link.jpg" name="Unknown" /> // Falls back to default
Dynamic Background Images
function HeroBanner({
title,
subtitle,
backgroundImage,
overlayOpacity = 0.5
}) {
return (
<section
style={{
position: "relative",
backgroundImage: `url(${backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
padding: "120px 40px",
color: "white",
}}
>
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
}}
/>
<div style={{ position: "relative", zIndex: 1 }}>
<h1 style={{ fontSize: "3rem", marginBottom: "1rem" }}>{title}</h1>
<p style={{ fontSize: "1.25rem" }}>{subtitle}</p>
</div>
</section>
);
}
<HeroBanner
title="Summer Collection"
subtitle="50% off all items"
backgroundImage="/images/summer-hero.jpg"
overlayOpacity={0.6}
/>
4. Dynamic Styles
Inline Styles from Props
function Box({
width = "auto",
height = "auto",
padding = "16px",
bgColor = "#ffffff",
borderRadius = "8px",
shadow = false,
children
}) {
const style = {
width,
height,
padding,
backgroundColor: bgColor,
borderRadius,
boxShadow: shadow ? "0 4px 12px rgba(0, 0, 0, 0.15)" : "none",
transition: "box-shadow 0.2s ease",
};
return <div style={style}>{children}</div>;
}
<Box bgColor="#f0f4f8" padding="24px" shadow>
<h2>Elevated Card</h2>
</Box>
Dynamic Style Objects Based on State/Props
function ProgressBar({ value, max = 100, color = "#2196f3", height = 8 }) {
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
// Dynamic color based on progress
const autoColor = percentage < 33
? "#f44336" // Red for low
: percentage < 66
? "#ff9800" // Orange for medium
: "#4caf50"; // Green for high
return (
<div
style={{
width: "100%",
height,
backgroundColor: "#e0e0e0",
borderRadius: height / 2,
overflow: "hidden",
}}
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
>
<div
style={{
width: `${percentage}%`,
height: "100%",
backgroundColor: color === "auto" ? autoColor : color,
borderRadius: height / 2,
transition: "width 0.3s ease",
}}
/>
</div>
);
}
<ProgressBar value={25} color="auto" /> // Red bar at 25%
<ProgressBar value={50} color="auto" /> // Orange bar at 50%
<ProgressBar value={90} color="auto" /> // Green bar at 90%
<ProgressBar value={75} color="#9c27b0" /> // Purple bar at 75%
5. Dynamic CSS Classes
String Concatenation
function Button({ variant = "primary", size = "medium", fullWidth = false, disabled = false, children, onClick }) {
// Build class string dynamically
let className = "btn";
className += ` btn-${variant}`;
className += ` btn-${size}`;
if (fullWidth) className += " btn-full-width";
if (disabled) className += " btn-disabled";
return (
<button className={className} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
// Renders: <button class="btn btn-primary btn-medium">Click</button>
<Button variant="primary" size="medium">Click</Button>
// Renders: <button class="btn btn-danger btn-large btn-full-width">Delete</button>
<Button variant="danger" size="large" fullWidth>Delete</Button>
Template Literals
function Card({ elevated = false, bordered = false, rounded = true, padding = "medium" }) {
const className = `
card
card-padding-${padding}
${elevated ? "card-elevated" : ""}
${bordered ? "card-bordered" : ""}
${rounded ? "card-rounded" : ""}
`.trim().replace(/\s+/g, " "); // Clean up whitespace
return <div className={className}>{children}</div>;
}
The clsx / classnames Library (Industry Standard)
npm install clsx
import clsx from "clsx";
function Button({ variant, size, fullWidth, disabled, active, children, onClick }) {
const className = clsx(
"btn", // Always included
`btn-${variant}`, // Always included (template)
`btn-${size}`, // Always included (template)
{
"btn-full-width": fullWidth, // Included only if fullWidth is truthy
"btn-disabled": disabled, // Included only if disabled is truthy
"btn-active": active, // Included only if active is truthy
}
);
return (
<button className={className} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
Tailwind CSS Dynamic Classes
function Badge({ variant = "default", size = "medium", children }) {
const variantClasses = {
default: "bg-gray-100 text-gray-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
danger: "bg-red-100 text-red-800",
info: "bg-blue-100 text-blue-800",
};
const sizeClasses = {
small: "text-xs px-2 py-0.5",
medium: "text-sm px-2.5 py-0.5",
large: "text-base px-3 py-1",
};
return (
<span className={`
inline-flex items-center rounded-full font-medium
${variantClasses[variant]}
${sizeClasses[size]}
`}>
{children}
</span>
);
}
6. Conditional Rendering Basics
React has no special if directive like Vue's v-if. You use plain JavaScript.
Pattern 1: Logical AND (&&)
// Shows the element ONLY if condition is true
function Notification({ count }) {
return (
<div>
<h1>Dashboard</h1>
{count > 0 && (
<div className="notification-banner">
You have {count} new notifications
</div>
)}
</div>
);
}
// ⚠️ GOTCHA: Watch out for falsy numbers!
function ItemCount({ count }) {
// ❌ BUG: When count is 0, renders the number 0 on screen
return <div>{count && <span>{count} items</span>}</div>;
// ✅ FIX: Explicitly check for > 0
return <div>{count > 0 && <span>{count} items</span>}</div>;
}
Pattern 2: Ternary Operator
// Choose between two elements
function LoginButton({ isLoggedIn, onLogin, onLogout }) {
return (
<button onClick={isLoggedIn ? onLogout : onLogin}>
{isLoggedIn ? "Log Out" : "Log In"}
</button>
);
}
// Nested ternaries (avoid nesting more than 2 levels!)
function StatusIcon({ status }) {
return (
<span>
{status === "success" ? "✅"
: status === "error" ? "❌"
: status === "loading" ? "⏳"
: "❓"}
</span>
);
}
Pattern 3: Early Return
// Return early for edge cases
function UserProfile({ user, isLoading, error }) {
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error.message} />;
}
if (!user) {
return <EmptyState message="User not found" />;
}
// Happy path — render the full component
return (
<div className="profile">
<Avatar src={user.avatar} name={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
Pattern 4: Object Lookup (Switch Alternative)
function Icon({ name, size = 24 }) {
const icons = {
home: <HomeIcon size={size} />,
settings: <SettingsIcon size={size} />,
profile: <ProfileIcon size={size} />,
search: <SearchIcon size={size} />,
mail: <MailIcon size={size} />,
};
return icons[name] || <DefaultIcon size={size} />;
}
// Status messages with object lookup
function StatusMessage({ status }) {
const messages = {
pending: { text: "Order received", color: "#ff9800", icon: "⏳" },
processing: { text: "Preparing your order", color: "#2196f3", icon: "🔄" },
shipped: { text: "On its way!", color: "#9c27b0", icon: "📦" },
delivered: { text: "Delivered", color: "#4caf50", icon: "✅" },
cancelled: { text: "Cancelled", color: "#f44336", icon: "❌" },
};
const info = messages[status] || { text: "Unknown", color: "#9e9e9e", icon: "❓" };
return (
<div style={{ color: info.color, display: "flex", alignItems: "center", gap: "8px" }}>
<span>{info.icon}</span>
<span>{info.text}</span>
</div>
);
}
7. Rendering Based on Data Shape
Real-world data isn't always clean. Dynamic rendering must handle variations.
function ContactCard({ contact }) {
return (
<div className="contact-card">
<h3>{contact.name}</h3>
{/* Only show if email exists */}
{contact.email && (
<p>
<strong>Email:</strong>{" "}
<a href={`mailto:${contact.email}`}>{contact.email}</a>
</p>
)}
{/* Only show if phone exists */}
{contact.phone && (
<p>
<strong>Phone:</strong>{" "}
<a href={`tel:${contact.phone}`}>{contact.phone}</a>
</p>
)}
{/* Only show if address is complete */}
{contact.address?.street && contact.address?.city && (
<p>
<strong>Address:</strong>{" "}
{contact.address.street}, {contact.address.city}
{contact.address.state && `, ${contact.address.state}`}
{contact.address.zip && ` ${contact.address.zip}`}
</p>
)}
{/* Show social links if any exist */}
{(contact.twitter || contact.github || contact.linkedin) && (
<div className="social-links">
{contact.twitter && <a href={`https://twitter.com/${contact.twitter}`}>Twitter</a>}
{contact.github && <a href={`https://github.com/${contact.github}`}>GitHub</a>}
{contact.linkedin && <a href={contact.linkedin}>LinkedIn</a>}
</div>
)}
{/* Fallback when no contact methods */}
{!contact.email && !contact.phone && (
<p className="no-contact">No contact information available</p>
)}
</div>
);
}
Handling Arrays of Different Lengths
function SkillBar({ skills }) {
if (!skills || skills.length === 0) {
return <p className="text-muted">No skills listed</p>;
}
// Show first 5, with "+N more" for overflow
const visible = skills.slice(0, 5);
const remaining = skills.length - 5;
return (
<div className="skill-bar">
{visible.map(skill => (
<span key={skill} className="skill-tag">{skill}</span>
))}
{remaining > 0 && (
<span className="skill-overflow">+{remaining} more</span>
)}
</div>
);
}
8. Component Variants with Props
The variant pattern is one of the most common in production React. A single component renders differently based on a variant prop.
function Alert({
variant = "info",
title,
children,
icon,
dismissible = false,
onDismiss
}) {
const config = {
info: {
bg: "#e3f2fd",
border: "#2196f3",
text: "#1565c0",
defaultIcon: "ℹ️",
},
success: {
bg: "#e8f5e9",
border: "#4caf50",
text: "#2e7d32",
defaultIcon: "✅",
},
warning: {
bg: "#fff3e0",
border: "#ff9800",
text: "#e65100",
defaultIcon: "⚠️",
},
error: {
bg: "#ffebee",
border: "#f44336",
text: "#c62828",
defaultIcon: "❌",
},
};
const c = config[variant] || config.info;
return (
<div
role="alert"
style={{
display: "flex",
alignItems: "flex-start",
gap: "12px",
padding: "16px",
backgroundColor: c.bg,
borderLeft: `4px solid ${c.border}`,
borderRadius: "4px",
color: c.text,
}}
>
<span style={{ fontSize: "1.25rem" }}>{icon || c.defaultIcon}</span>
<div style={{ flex: 1 }}>
{title && <strong style={{ display: "block", marginBottom: "4px" }}>{title}</strong>}
<div>{children}</div>
</div>
{dismissible && onDismiss && (
<button
onClick={onDismiss}
style={{
background: "none",
border: "none",
fontSize: "1.25rem",
cursor: "pointer",
color: c.text,
padding: 0,
}}
aria-label="Dismiss"
>
×
</button>
)}
</div>
);
}
// Usage
<Alert variant="success" title="Payment Received">
Your transaction was processed successfully.
</Alert>
<Alert variant="error" title="Upload Failed" dismissible onDismiss={handleDismiss}>
The file exceeded the 5MB limit. Please try again.
</Alert>
9. Status Indicators and Badges
Online Status Indicator
function StatusDot({ status, showLabel = false, size = 10 }) {
const statusMap = {
online: { color: "#4caf50", label: "Online" },
offline: { color: "#9e9e9e", label: "Offline" },
busy: { color: "#f44336", label: "Do not disturb" },
away: { color: "#ff9800", label: "Away" },
idle: { color: "#ffc107", label: "Idle" },
};
const info = statusMap[status] || statusMap.offline;
return (
<span style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
<span
style={{
display: "inline-block",
width: size,
height: size,
borderRadius: "50%",
backgroundColor: info.color,
// Pulse animation for "online"
animation: status === "online" ? "pulse 2s infinite" : "none",
}}
title={info.label}
/>
{showLabel && <span style={{ fontSize: "0.875rem" }}>{info.label}</span>}
</span>
);
}
Notification Badge
function NotificationBadge({ count, max = 99, children }) {
if (!count || count <= 0) return <>{children}</>;
const display = count > max ? `${max}+` : String(count);
return (
<div style={{ position: "relative", display: "inline-block" }}>
{children}
<span
style={{
position: "absolute",
top: "-8px",
right: "-8px",
minWidth: "20px",
height: "20px",
padding: "0 6px",
fontSize: "0.75rem",
fontWeight: "bold",
color: "white",
backgroundColor: "#f44336",
borderRadius: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
lineHeight: 1,
}}
>
{display}
</span>
</div>
);
}
// Usage
<NotificationBadge count={5}>
<MailIcon />
</NotificationBadge>
<NotificationBadge count={150} max={99}>
<BellIcon />
</NotificationBadge>
// Shows "99+"
Priority Tag
function PriorityTag({ level }) {
const config = {
critical: { bg: "#d32f2f", text: "white", label: "Critical" },
high: { bg: "#f57c00", text: "white", label: "High" },
medium: { bg: "#fbc02d", text: "#333", label: "Medium" },
low: { bg: "#4caf50", text: "white", label: "Low" },
none: { bg: "#e0e0e0", text: "#666", label: "None" },
};
const c = config[level] || config.none;
return (
<span style={{
display: "inline-block",
padding: "2px 10px",
fontSize: "0.75rem",
fontWeight: 600,
borderRadius: "12px",
backgroundColor: c.bg,
color: c.text,
textTransform: "uppercase",
letterSpacing: "0.5px",
}}>
{c.label}
</span>
);
}
10. Role-Based and Permission-Based UI
function Dashboard({ user }) {
return (
<div>
<h1>Dashboard</h1>
{/* Everyone sees this */}
<UserProfile user={user} />
{/* Only admins and moderators */}
{(user.role === "admin" || user.role === "moderator") && (
<ModeratorPanel />
)}
{/* Only admins */}
{user.role === "admin" && (
<AdminSettings />
)}
{/* Feature flags */}
{user.features?.includes("beta_dashboard") && (
<BetaDashboard />
)}
</div>
);
}
// Reusable permission gate component
function RequireRole({ user, roles, children, fallback = null }) {
if (!roles.includes(user.role)) {
return fallback;
}
return children;
}
// Usage
<RequireRole user={user} roles={["admin", "moderator"]}>
<DeleteButton />
</RequireRole>
<RequireRole
user={user}
roles={["admin"]}
fallback={<p>You don't have permission to view this.</p>}
>
<AdminPanel />
</RequireRole>
Permission-Based with Granular Checks
function ActionBar({ permissions, onEdit, onDelete, onPublish, onArchive }) {
return (
<div className="action-bar">
{permissions.canEdit && (
<button onClick={onEdit} className="btn-edit">Edit</button>
)}
{permissions.canDelete && (
<button onClick={onDelete} className="btn-delete">Delete</button>
)}
{permissions.canPublish && (
<button onClick={onPublish} className="btn-publish">Publish</button>
)}
{permissions.canArchive && (
<button onClick={onArchive} className="btn-archive">Archive</button>
)}
{/* Show message if no actions available */}
{!permissions.canEdit && !permissions.canDelete &&
!permissions.canPublish && !permissions.canArchive && (
<p className="text-muted">No actions available</p>
)}
</div>
);
}
// Usage
<ActionBar
permissions={{ canEdit: true, canDelete: false, canPublish: true, canArchive: false }}
onEdit={handleEdit}
onPublish={handlePublish}
/>
11. Dynamic Component Selection
Sometimes the component itself changes based on data.
Using an Object Map
import HomeIcon from "./icons/HomeIcon";
import SettingsIcon from "./icons/SettingsIcon";
import UserIcon from "./icons/UserIcon";
import MailIcon from "./icons/MailIcon";
const iconMap = {
home: HomeIcon,
settings: SettingsIcon,
user: UserIcon,
mail: MailIcon,
};
function DynamicIcon({ name, ...props }) {
const IconComponent = iconMap[name];
if (!IconComponent) {
console.warn(`Icon "${name}" not found`);
return null;
}
return <IconComponent {...props} />;
}
// Usage
<DynamicIcon name="home" size={24} color="blue" />
<DynamicIcon name="settings" size={32} />
Dynamic Form Fields
const fieldComponents = {
text: TextInput,
email: EmailInput,
password: PasswordInput,
select: SelectInput,
textarea: TextAreaInput,
checkbox: CheckboxInput,
radio: RadioGroup,
};
function DynamicForm({ fields, values, onChange }) {
return (
<form>
{fields.map(field => {
const FieldComponent = fieldComponents[field.type];
if (!FieldComponent) {
console.warn(`Unknown field type: ${field.type}`);
return null;
}
return (
<FieldComponent
key={field.name}
label={field.label}
name={field.name}
value={values[field.name]}
onChange={(value) => onChange(field.name, value)}
required={field.required}
options={field.options}
placeholder={field.placeholder}
/>
);
})}
</form>
);
}
// Usage — form structure driven by data
const formFields = [
{ type: "text", name: "firstName", label: "First Name", required: true },
{ type: "text", name: "lastName", label: "Last Name", required: true },
{ type: "email", name: "email", label: "Email", required: true },
{ type: "select", name: "role", label: "Role", options: ["User", "Admin", "Mod"] },
{ type: "textarea", name: "bio", label: "Bio", placeholder: "Tell us about yourself..." },
];
<DynamicForm fields={formFields} values={formValues} onChange={handleFieldChange} />
12. Template Patterns for Real UIs
Product Card (E-Commerce)
function ProductCard({ product }) {
const { name, price, originalPrice, image, rating, reviewCount, inStock, badge } = product;
const discount = originalPrice
? Math.round((1 - price / originalPrice) * 100)
: 0;
return (
<div className="product-card">
{/* Image with badge */}
<div style={{ position: "relative" }}>
<img src={image} alt={name} style={{ width: "100%", aspectRatio: "1", objectFit: "cover" }} />
{badge && (
<span style={{
position: "absolute", top: 8, left: 8,
padding: "4px 8px", borderRadius: "4px",
backgroundColor: badge === "New" ? "#2196f3" : "#f44336",
color: "white", fontSize: "0.75rem", fontWeight: "bold",
}}>
{badge}
</span>
)}
{!inStock && (
<div style={{
position: "absolute", inset: 0,
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex", alignItems: "center", justifyContent: "center",
color: "white", fontWeight: "bold", fontSize: "1.25rem",
}}>
Out of Stock
</div>
)}
</div>
{/* Details */}
<div style={{ padding: "12px" }}>
<h3 style={{ fontSize: "1rem", marginBottom: "8px" }}>{name}</h3>
{/* Rating */}
<div style={{ display: "flex", alignItems: "center", gap: "4px", marginBottom: "8px" }}>
{"★".repeat(Math.round(rating))}{"☆".repeat(5 - Math.round(rating))}
<span style={{ fontSize: "0.875rem", color: "#666" }}>({reviewCount})</span>
</div>
{/* Price */}
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "1.25rem", fontWeight: "bold" }}>
${price.toFixed(2)}
</span>
{originalPrice && (
<>
<span style={{ textDecoration: "line-through", color: "#999", fontSize: "0.875rem" }}>
${originalPrice.toFixed(2)}
</span>
<span style={{ color: "#f44336", fontSize: "0.875rem", fontWeight: "bold" }}>
-{discount}%
</span>
</>
)}
</div>
</div>
</div>
);
}
Comment Thread
function Comment({ author, avatar, content, timestamp, likes, replies = [], depth = 0 }) {
const maxDepth = 3;
return (
<div style={{ marginLeft: depth > 0 ? 24 : 0, borderLeft: depth > 0 ? "2px solid #e0e0e0" : "none", paddingLeft: depth > 0 ? 16 : 0 }}>
<div style={{ display: "flex", gap: "12px", marginBottom: "8px" }}>
<img src={avatar} alt={author} style={{ width: 32, height: 32, borderRadius: "50%" }} />
<div>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<strong>{author}</strong>
<span style={{ color: "#999", fontSize: "0.875rem" }}>{timestamp}</span>
</div>
<p style={{ margin: "4px 0" }}>{content}</p>
<div style={{ display: "flex", gap: "16px", fontSize: "0.875rem", color: "#666" }}>
<button style={{ background: "none", border: "none", cursor: "pointer" }}>
❤️ {likes}
</button>
{depth < maxDepth && (
<button style={{ background: "none", border: "none", cursor: "pointer" }}>
Reply
</button>
)}
</div>
</div>
</div>
{/* Nested replies */}
{replies.length > 0 && depth < maxDepth && (
<div>
{replies.map(reply => (
<Comment key={reply.id} {...reply} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
13. Performance Considerations
Avoid Creating Objects/Arrays Inside JSX
// ❌ BAD — new object every render, causes child to re-render
function Parent() {
return <Child style={{ color: "red" }} items={[1, 2, 3]} />;
}
// ✅ GOOD — stable references
const childStyle = { color: "red" };
const items = [1, 2, 3];
function Parent() {
return <Child style={childStyle} items={items} />;
}
// ✅ ALSO GOOD — useMemo for dynamic values
function Parent({ color }) {
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}
Avoid Inline Function Props When Possible
// ❌ New function on every render
function TodoList({ todos, onToggle }) {
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)} // New function each render
/>
));
}
// ✅ Pass ID as prop, handle in child
function TodoList({ todos, onToggle }) {
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle} // Stable reference
/>
));
}
function TodoItem({ todo, onToggle }) {
const handleToggle = () => onToggle(todo.id); // Create in child
return <li onClick={handleToggle}>{todo.text}</li>;
}
14. Common Mistakes
Mistake 1: Rendering Objects Directly
// ❌ CRASHES — React can't render objects
function UserInfo({ user }) {
return <p>{user}</p>; // Error: Objects are not valid as a React child
}
// ✅ Access specific properties
function UserInfo({ user }) {
return <p>{user.name} ({user.email})</p>;
}
Mistake 2: Forgetting that 0 Renders
// ❌ Shows "0" on screen when count is 0
function Notifications({ count }) {
return <div>{count && <Badge count={count} />}</div>;
}
// ✅ Explicit boolean check
function Notifications({ count }) {
return <div>{count > 0 && <Badge count={count} />}</div>;
}
// ✅ Or use ternary
function Notifications({ count }) {
return <div>{count > 0 ? <Badge count={count} /> : null}</div>;
}
Mistake 3: Complex Ternary Nesting
// ❌ Unreadable nested ternaries
return (
<div>
{status === "loading" ? <Spinner /> : status === "error" ? <Error /> : status === "empty" ? <Empty /> : <Data />}
</div>
);
// ✅ Use early returns or object lookup
function Content({ status, data, error }) {
if (status === "loading") return <Spinner />;
if (status === "error") return <Error message={error} />;
if (status === "empty") return <Empty />;
return <DataDisplay data={data} />;
}
15. Key Takeaways
- Dynamic rendering = UI that changes based on data, not hardcoded content
- Use
{}in JSX to insert any JavaScript expression — variables, function calls, ternaries - Format data before rendering — numbers, dates, currencies should be human-readable
- Dynamic classes — use
clsxor template literals for conditional CSS classes - Conditional rendering —
&&, ternary, early return, object lookup are your four tools - Watch for
0and""— these are falsy but renderable; use explicit boolean checks - Component variants — one component with a
variantprop beats multiple similar components - Dynamic component selection — use object maps to pick components based on data
- Role/permission-based UI — control visibility with boolean checks or gate components
- Performance — avoid creating new objects/arrays/functions inside JSX on every render
Explain-It Challenge
-
The Newspaper: Imagine a newspaper template where the layout stays the same but content changes daily. How does this relate to React's dynamic rendering? What are the "props" of a newspaper? What would a "hardcoded" newspaper look like?
-
The Bouncer: Explain conditional rendering using the analogy of a bouncer at a club. Different conditions (age, VIP status, dress code) determine who gets in. How does this map to
&&, ternary, and early return patterns? -
The Custom T-Shirt Shop: A shop has one T-shirt template but can customize color, text, size, and print design. Explain how this maps to a React component with variant props. Why is one flexible component better than 50 hardcoded variations?
Navigation: ← 2.2.b — Understanding Props · Next → 2.2.d — Rendering Lists