Difficulty: Beginner
Dynamic rendering is the practice of building UI that adapts entirely to the data it receives. Instead of hardcoding specific elements, you write components that render whatever data is passed to them, handling every possible state: loading, error, empty, and success. This is how real-world React applications work - the UI is a function of data, and it must gracefully handle all data states.
Loading states inform the user that data is being fetched. A good loading state prevents the UI from looking broken or empty while data is in transit. Common patterns include spinner icons, skeleton screens (placeholder shapes that mimic the final layout), and progress bars. In React, you typically track loading state with a boolean: `const [isLoading, setIsLoading] = useState(true)`, and conditionally render a loading indicator.
Empty states handle the case where data loaded successfully but there are zero results. Instead of showing a blank page, a good empty state tells the user what happened and what they can do. For example, a search with no results might show 'No results found for [query]. Try different keywords.' An empty todo list might show 'No tasks yet. Add your first task!' Empty states turn a potentially confusing blank screen into a helpful, guiding experience.
Error states handle failures like network errors, server errors, or unauthorized access. A good error state shows a human-readable message, possibly offers a retry action, and avoids exposing technical details to the user. In React, you store error information in state: `const [error, setError] = useState(null)`, and render an error UI when it is set. Error boundaries (class components with componentDidCatch) catch rendering errors in child components.
The typical pattern for dynamic rendering is a state machine with three possible states: loading, error, and success. Your component renders different UI for each state. This can be expressed cleanly with early returns: `if (loading) return <Loading />;` `if (error) return <Error />;` `return <Content data={data} />;`. This pattern ensures your component handles every scenario and never shows an unexpected blank or broken state.
function renderUserList(data) {
if (data.loading) {
return 'Loading users...';
}
if (data.error) {
return 'Failed to load: ' + data.error;
}
if (!data.users || data.users.length === 0) {
return 'No users found. Invite someone!';
}
return data.users.map(u => u.name).join(', ');
}
console.log(renderUserList({ loading: true }));
console.log(renderUserList({ loading: false, error: 'Server down' }));
console.log(renderUserList({ loading: false, users: [] }));
console.log(renderUserList({ loading: false, users: [{ name: 'Alice' }, { name: 'Bob' }] }));
The function handles all four states: loading, error, empty, and success. Each state produces meaningful output instead of a blank or broken UI.
function createSkeleton(count) {
return Array.from({ length: count }, (_, i) => ({
id: 'skeleton-' + i,
display: '[████████████ Loading... ████████████]'
}));
}
function renderCards(state) {
if (state.loading) {
const skeletons = createSkeleton(3);
console.log('Showing skeletons:');
skeletons.forEach(s => console.log(' ' + s.display));
return;
}
console.log('Showing real data:');
state.items.forEach(item => {
console.log(' ' + item.title + ' - ' + item.desc);
});
}
renderCards({ loading: true });
console.log('---');
renderCards({
loading: false,
items: [
{ title: 'React', desc: 'UI library' },
{ title: 'Vue', desc: 'Progressive framework' }
]
});
Skeleton patterns show placeholder content that mimics the layout of real data. This provides a smoother experience than a generic spinner.
function renderSearchResults(query, results) {
if (!query) {
return 'Enter a search term to get started';
}
if (results.length === 0) {
return 'No results for "' + query + '". Try different keywords.';
}
if (results.length === 1) {
return 'Found 1 result for "' + query + '": ' + results[0];
}
return 'Found ' + results.length + ' results for "' + query + '": ' + results.join(', ');
}
console.log(renderSearchResults('', []));
console.log(renderSearchResults('xyz', []));
console.log(renderSearchResults('React', ['React Docs']));
console.log(renderSearchResults('JS', ['JavaScript', 'JSX', 'JSON']));
Different empty states for different contexts: no query vs no results. Each provides helpful guidance to the user.
data-driven rendering, empty states, loading states, error states, skeleton patterns