Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering
2.5.d — Dynamic UI Rendering
In one sentence: Dynamic UI rendering combines state management with conditional rendering to build interactive elements like tabs, accordions, modals, and toggling visibility — all driven by state changes that React re-renders automatically.
Navigation: ← Conditional Rendering Patterns · Next → Practical Build: Dynamic Form
Table of Contents
- What is Dynamic UI?
- Show/Hide Toggle
- Tab Component
- Accordion / Collapsible
- Modal / Dialog
- Dropdown Menu
- Notification / Toast System
- Dynamic Component Selection
- Multi-Step Wizard
- Toggle Between Views (Grid vs List)
- Skeleton Loading Screens
- Animated Transitions
- Accessibility Considerations
- Key Takeaways
1. What is Dynamic UI?
Dynamic UI means components that change their visual state in response to user interaction — without page reloads. Every dynamic UI pattern follows the same loop:
┌──────────────────────────────────┐
│ User Action │
│ (click, hover, scroll, etc.) │
│ │ │
│ ▼ │
│ State Change │
│ (useState, useReducer) │
│ │ │
│ ▼ │
│ React Re-renders │
│ │ │
│ ▼ │
│ Conditional Rendering │
│ decides what to show │
│ │ │
│ ▼ │
│ Updated UI │
└──────────────────────────────────┘
2. Show/Hide Toggle
Simple Toggle
function ToggleSection({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '▼' : '▶'} {title}
</button>
{isOpen && <div style={{ padding: '12px 0' }}>{children}</div>}
</div>
);
}
// Usage
<ToggleSection title="Advanced Settings">
<p>Secret options here...</p>
</ToggleSection>
Toggle with Animation Prep
function AnimatedToggle({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
const contentRef = useRef(null);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{title} {isOpen ? '−' : '+'}
</button>
<div
ref={contentRef}
style={{
maxHeight: isOpen ? contentRef.current?.scrollHeight + 'px' : '0px',
overflow: 'hidden',
transition: 'max-height 0.3s ease',
}}
>
<div style={{ padding: '12px 0' }}>{children}</div>
</div>
</div>
);
}
3. Tab Component
Basic Tabs
function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
{/* Tab Headers */}
<div style={{ display: 'flex', borderBottom: '2px solid #eee' }}>
{tabs.map((tab, index) => (
<button
key={tab.label}
onClick={() => setActiveTab(index)}
style={{
padding: '12px 24px',
border: 'none',
background: 'none',
cursor: 'pointer',
borderBottom: activeTab === index ? '2px solid #0070f3' : '2px solid transparent',
color: activeTab === index ? '#0070f3' : '#666',
fontWeight: activeTab === index ? 600 : 400,
marginBottom: -2,
}}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
<div style={{ padding: 20 }}>
{tabs[activeTab].content}
</div>
</div>
);
}
// Usage
function App() {
const tabs = [
{ label: 'Profile', content: <ProfileTab /> },
{ label: 'Settings', content: <SettingsTab /> },
{ label: 'Billing', content: <BillingTab /> },
];
return <Tabs tabs={tabs} />;
}
Tabs That Preserve State
By default, switching tabs unmounts the previous content. To preserve state:
function StatefulTabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
<div style={{ display: 'flex', gap: 4 }}>
{tabs.map((tab, index) => (
<button
key={tab.label}
onClick={() => setActiveTab(index)}
style={{
padding: '8px 16px',
background: activeTab === index ? '#0070f3' : '#f0f0f0',
color: activeTab === index ? 'white' : '#333',
border: 'none',
borderRadius: '8px 8px 0 0',
cursor: 'pointer',
}}
>
{tab.label}
</button>
))}
</div>
{/* ALL tabs are rendered, but only active one is visible */}
{tabs.map((tab, index) => (
<div
key={tab.label}
style={{ display: activeTab === index ? 'block' : 'none', padding: 20 }}
>
{tab.content}
</div>
))}
</div>
);
}
4. Accordion / Collapsible
Single-Open Accordion
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState(null);
function handleToggle(index) {
setOpenIndex(prev => (prev === index ? null : index));
}
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
{items.map((item, index) => (
<div key={index}>
<button
onClick={() => handleToggle(index)}
style={{
width: '100%',
padding: '16px 20px',
background: openIndex === index ? '#f7f7f7' : 'white',
border: 'none',
borderBottom: '1px solid #eee',
textAlign: 'left',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontWeight: 500,
}}
>
{item.title}
<span style={{
transform: openIndex === index ? 'rotate(180deg)' : 'rotate(0)',
transition: 'transform 0.2s',
}}>
▼
</span>
</button>
{openIndex === index && (
<div style={{ padding: '16px 20px', background: '#fafafa' }}>
{item.content}
</div>
)}
</div>
))}
</div>
);
}
// Usage
const faqItems = [
{ title: 'What is React?', content: 'React is a JavaScript library for building UIs.' },
{ title: 'What is JSX?', content: 'JSX is a syntax extension that lets you write HTML-like code in JavaScript.' },
{ title: 'What are hooks?', content: 'Hooks let you use state and other React features in functional components.' },
];
<Accordion items={faqItems} />
Multi-Open Accordion
function MultiAccordion({ items }) {
const [openItems, setOpenItems] = useState(new Set());
function handleToggle(index) {
setOpenItems(prev => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
}
return (
<div>
{items.map((item, index) => (
<div key={index} style={{ marginBottom: 4 }}>
<button
onClick={() => handleToggle(index)}
style={{
width: '100%',
padding: 16,
textAlign: 'left',
border: '1px solid #ddd',
borderRadius: openItems.has(index) ? '8px 8px 0 0' : 8,
background: openItems.has(index) ? '#f0f0f0' : 'white',
cursor: 'pointer',
}}
>
{openItems.has(index) ? '−' : '+'} {item.title}
</button>
{openItems.has(index) && (
<div style={{
padding: 16,
border: '1px solid #ddd',
borderTop: 'none',
borderRadius: '0 0 8px 8px',
}}>
{item.content}
</div>
)}
</div>
))}
</div>
);
}
5. Modal / Dialog
Complete Modal Component
function Modal({ isOpen, onClose, title, children }) {
// Close on Escape key
useEffect(() => {
function handleEscape(event) {
if (event.key === 'Escape') onClose();
}
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden'; // Prevent background scroll
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose} // Close on backdrop click
>
<div
style={{
background: 'white',
borderRadius: 12,
padding: 24,
maxWidth: 500,
width: '90%',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()} // Don't close when clicking content
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}>{title}</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: 24,
cursor: 'pointer',
padding: '0 4px',
}}
aria-label="Close modal"
>
×
</button>
</div>
{children}
</div>
</div>,
document.body
);
}
// Usage
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Open Modal</button>
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Confirm Action"
>
<p>Are you sure you want to proceed?</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
<button onClick={() => setShowModal(false)}>Cancel</button>
<button onClick={() => { handleConfirm(); setShowModal(false); }}>
Confirm
</button>
</div>
</Modal>
</div>
);
}
6. Dropdown Menu
function Dropdown({ trigger, items }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
// Close on outside click
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<button onClick={() => setIsOpen(!isOpen)}>
{trigger} {isOpen ? '▲' : '▼'}
</button>
{isOpen && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
marginTop: 4,
background: 'white',
border: '1px solid #ddd',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
minWidth: 160,
zIndex: 100,
overflow: 'hidden',
}}>
{items.map((item, i) => (
item.divider ? (
<hr key={i} style={{ margin: '4px 0', border: 'none', borderTop: '1px solid #eee' }} />
) : (
<button
key={i}
onClick={() => {
item.onClick();
setIsOpen(false);
}}
style={{
display: 'block',
width: '100%',
padding: '10px 16px',
border: 'none',
background: 'none',
textAlign: 'left',
cursor: 'pointer',
fontSize: 14,
}}
onMouseEnter={(e) => e.target.style.background = '#f5f5f5'}
onMouseLeave={(e) => e.target.style.background = 'none'}
>
{item.icon && <span style={{ marginRight: 8 }}>{item.icon}</span>}
{item.label}
</button>
)
))}
</div>
)}
</div>
);
}
// Usage
<Dropdown
trigger="Actions"
items={[
{ label: 'Edit', icon: '✏️', onClick: () => console.log('Edit') },
{ label: 'Duplicate', icon: '📋', onClick: () => console.log('Duplicate') },
{ divider: true },
{ label: 'Delete', icon: '🗑️', onClick: () => console.log('Delete') },
]}
/>
7. Notification / Toast System
function useToasts() {
const [toasts, setToasts] = useState([]);
function addToast(message, type = 'info', duration = 3000) {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
if (duration > 0) {
setTimeout(() => removeToast(id), duration);
}
}
function removeToast(id) {
setToasts(prev => prev.filter(t => t.id !== id));
}
return { toasts, addToast, removeToast };
}
function ToastContainer({ toasts, removeToast }) {
const typeStyles = {
success: { bg: '#d4edda', color: '#155724', icon: '✅' },
error: { bg: '#f8d7da', color: '#721c24', icon: '❌' },
warning: { bg: '#fff3cd', color: '#856404', icon: '⚠️' },
info: { bg: '#d1ecf1', color: '#0c5460', icon: 'ℹ️' },
};
return (
<div style={{ position: 'fixed', top: 20, right: 20, zIndex: 9999 }}>
{toasts.map(toast => {
const s = typeStyles[toast.type] || typeStyles.info;
return (
<div
key={toast.id}
style={{
background: s.bg,
color: s.color,
padding: '12px 16px',
borderRadius: 8,
marginBottom: 8,
display: 'flex',
alignItems: 'center',
gap: 8,
minWidth: 250,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}
>
<span>{s.icon}</span>
<span style={{ flex: 1 }}>{toast.message}</span>
<button
onClick={() => removeToast(toast.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 16 }}
>
×
</button>
</div>
);
})}
</div>
);
}
// Usage
function App() {
const { toasts, addToast, removeToast } = useToasts();
return (
<div>
<button onClick={() => addToast('Saved!', 'success')}>Success</button>
<button onClick={() => addToast('Something failed', 'error')}>Error</button>
<button onClick={() => addToast('Check this out', 'warning')}>Warning</button>
<ToastContainer toasts={toasts} removeToast={removeToast} />
</div>
);
}
8. Dynamic Component Selection
Component Map
const componentMap = {
text: TextBlock,
image: ImageBlock,
video: VideoBlock,
code: CodeBlock,
quote: QuoteBlock,
};
function ContentRenderer({ blocks }) {
return (
<div>
{blocks.map(block => {
const Component = componentMap[block.type];
if (!Component) return null;
return <Component key={block.id} {...block.props} />;
})}
</div>
);
}
// Usage
const blocks = [
{ id: 1, type: 'text', props: { content: 'Hello world' } },
{ id: 2, type: 'image', props: { src: '/photo.jpg', alt: 'Photo' } },
{ id: 3, type: 'code', props: { language: 'javascript', code: 'const x = 1;' } },
];
<ContentRenderer blocks={blocks} />
Form Field Factory
const fieldComponents = {
text: ({ name, label, value, onChange }) => (
<input type="text" name={name} value={value} onChange={onChange} placeholder={label} />
),
textarea: ({ name, label, value, onChange }) => (
<textarea name={name} value={value} onChange={onChange} placeholder={label} rows={4} />
),
select: ({ name, label, value, onChange, options }) => (
<select name={name} value={value} onChange={onChange}>
<option value="">{label}</option>
{options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
),
checkbox: ({ name, label, value, onChange }) => (
<label>
<input type="checkbox" name={name} checked={value} onChange={onChange} />
{' '}{label}
</label>
),
};
function DynamicForm({ fields, values, onChange }) {
return (
<form>
{fields.map(field => {
const FieldComponent = fieldComponents[field.type];
if (!FieldComponent) return null;
return (
<div key={field.name} style={{ marginBottom: 12 }}>
<FieldComponent
{...field}
value={values[field.name] || ''}
onChange={onChange}
/>
</div>
);
})}
</form>
);
}
9. Multi-Step Wizard
function Wizard({ steps }) {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({});
function handleNext(stepData) {
setFormData(prev => ({ ...prev, ...stepData }));
setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
}
function handleBack() {
setCurrentStep(prev => Math.max(prev - 1, 0));
}
function handleSubmit(stepData) {
const finalData = { ...formData, ...stepData };
console.log('Complete form data:', finalData);
}
const StepComponent = steps[currentStep].component;
const isFirst = currentStep === 0;
const isLast = currentStep === steps.length - 1;
return (
<div>
{/* Progress Indicator */}
<div style={{ display: 'flex', marginBottom: 24 }}>
{steps.map((step, i) => (
<div key={i} style={{ flex: 1, textAlign: 'center' }}>
<div style={{
width: 32,
height: 32,
borderRadius: '50%',
background: i <= currentStep ? '#0070f3' : '#ddd',
color: 'white',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
}}>
{i < currentStep ? '✓' : i + 1}
</div>
<p style={{
fontSize: 12,
color: i <= currentStep ? '#0070f3' : '#999',
marginTop: 4,
}}>
{step.title}
</p>
</div>
))}
</div>
{/* Step Content */}
<StepComponent
data={formData}
onNext={isLast ? handleSubmit : handleNext}
onBack={!isFirst ? handleBack : undefined}
isLast={isLast}
/>
</div>
);
}
// Step Components
function PersonalInfo({ data, onNext, onBack }) {
const [name, setName] = useState(data.name || '');
const [email, setEmail] = useState(data.email || '');
return (
<div>
<h2>Personal Information</h2>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
{onBack && <button onClick={onBack}>Back</button>}
<button onClick={() => onNext({ name, email })}>Next</button>
</div>
</div>
);
}
// Usage
<Wizard
steps={[
{ title: 'Personal', component: PersonalInfo },
{ title: 'Address', component: AddressForm },
{ title: 'Payment', component: PaymentForm },
{ title: 'Review', component: ReviewStep },
]}
/>
10. Toggle Between Views (Grid vs List)
function ViewToggle({ items }) {
const [view, setView] = useState('grid');
return (
<div>
{/* View Switcher */}
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
<button
onClick={() => setView('grid')}
style={{
padding: '8px 16px',
background: view === 'grid' ? '#0070f3' : '#f0f0f0',
color: view === 'grid' ? 'white' : '#333',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
▦ Grid
</button>
<button
onClick={() => setView('list')}
style={{
padding: '8px 16px',
background: view === 'list' ? '#0070f3' : '#f0f0f0',
color: view === 'list' ? 'white' : '#333',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
☰ List
</button>
</div>
{/* Content */}
{view === 'grid' ? (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 16,
}}>
{items.map(item => (
<div key={item.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<img src={item.image} alt={item.name} style={{ width: '100%', borderRadius: 4 }} />
<h3>{item.name}</h3>
<p>{item.price}</p>
</div>
))}
</div>
) : (
<div>
{items.map(item => (
<div key={item.id} style={{
display: 'flex',
gap: 16,
padding: 12,
borderBottom: '1px solid #eee',
alignItems: 'center',
}}>
<img src={item.image} alt={item.name} style={{ width: 60, height: 60, borderRadius: 4 }} />
<div>
<h3 style={{ margin: 0 }}>{item.name}</h3>
<p style={{ margin: 0, color: '#666' }}>{item.price}</p>
</div>
</div>
))}
</div>
)}
</div>
);
}
11. Skeleton Loading Screens
function SkeletonCard() {
return (
<div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, width: 250 }}>
{/* Image placeholder */}
<div style={{
height: 150,
borderRadius: 4,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
{/* Title placeholder */}
<div style={{
height: 20,
width: '80%',
marginTop: 12,
borderRadius: 4,
background: '#f0f0f0',
}} />
{/* Description placeholder */}
<div style={{
height: 14,
width: '60%',
marginTop: 8,
borderRadius: 4,
background: '#f0f0f0',
}} />
</div>
);
}
function ProductList({ isLoading, products }) {
if (isLoading) {
return (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
);
}
return (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
12. Animated Transitions
CSS Transition on State Change
function FadeToggle() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
<div style={{
opacity: isVisible ? 1 : 0,
transform: isVisible ? 'translateY(0)' : 'translateY(-10px)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: isVisible ? 'auto' : 'none',
}}>
<p>I fade in and out!</p>
</div>
</div>
);
}
Slide Transition for Tabs
function SlidingTabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
const [direction, setDirection] = useState('right');
function handleTabChange(newIndex) {
setDirection(newIndex > activeTab ? 'right' : 'left');
setActiveTab(newIndex);
}
return (
<div>
<div style={{ display: 'flex', gap: 4 }}>
{tabs.map((tab, i) => (
<button key={i} onClick={() => handleTabChange(i)}>
{tab.label}
</button>
))}
</div>
<div style={{ overflow: 'hidden' }}>
<div
key={activeTab}
style={{
animation: `slide-${direction} 0.3s ease`,
}}
>
{tabs[activeTab].content}
</div>
</div>
</div>
);
}
13. Accessibility Considerations
Modal Accessibility
function AccessibleModal({ isOpen, onClose, title, children }) {
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement;
// Focus will be managed by the dialog
}
return () => {
// Restore focus when modal closes
previousFocusRef.current?.focus();
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close dialog">×</button>
</div>
);
}
Accordion Accessibility
function AccessibleAccordion({ items }) {
const [openIndex, setOpenIndex] = useState(null);
return (
<div>
{items.map((item, index) => {
const isOpen = openIndex === index;
const panelId = `panel-${index}`;
const headerId = `header-${index}`;
return (
<div key={index}>
<h3>
<button
id={headerId}
onClick={() => setOpenIndex(isOpen ? null : index)}
aria-expanded={isOpen}
aria-controls={panelId}
style={{ width: '100%', textAlign: 'left' }}
>
{item.title}
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!isOpen}
>
{item.content}
</div>
</div>
);
})}
</div>
);
}
ARIA Attributes Quick Reference
| Component | Required ARIA | Purpose |
|---|---|---|
| Modal | role="dialog", aria-modal="true", aria-labelledby | Announces dialog to screen readers |
| Tab | role="tablist", role="tab", aria-selected | Tab navigation pattern |
| Accordion | aria-expanded, aria-controls | Expandable sections |
| Dropdown | aria-haspopup, aria-expanded | Menu disclosure |
| Toggle | aria-pressed or aria-checked | On/off state |
14. Key Takeaways
- All dynamic UI follows the same pattern: user action → state change → re-render → conditional rendering
- Tabs: Track
activeTabindex, render content based on selection. Usedisplay: noneto preserve state across tabs. - Modals: Use
createPortal, handle Escape key, prevent background scroll, stop propagation on content click - Accordions: Single-open =
openIndexstate; multi-open =Setstate - Dropdowns: Close on outside click with
useEffect+contains()check - Component map pattern = most flexible way to render dynamic component types
- Skeleton screens provide better UX than spinners — they show layout shape during loading
- CSS transitions (
opacity,transform,max-height) give smooth animations without libraries - Always consider accessibility — ARIA roles, keyboard navigation, focus management
- Preserve vs destroy state:
{show && <X/>}destroys state;display: nonepreserves it
Explain-It Challenge
-
The Hotel Analogy: Explain the difference between conditional rendering (
{show && <Modal />}) and CSS hiding (display: none) using the analogy of hotel rooms. When is a room demolished vs just locked? -
The Accessibility Audit: You're given a modal that opens correctly but has no keyboard support. Walk through the minimum accessibility requirements: focus trapping, Escape to close, aria attributes, and focus restoration.
-
The Performance Discussion: You have a tab interface with 4 heavy tabs. Each tab fetches data on mount. Discuss the trade-offs between mounting/unmounting tabs on switch vs rendering all and using
display: none.
Navigation: ← Conditional Rendering Patterns · Next → Practical Build: Dynamic Form