Loading & Error States
Lerne, wie du Loading-Zustände und Fehler in React professionell behandelst. Skeleton-Loader, Retry-Logik und Error Boundaries.
Loading & Error States
Eine gute App zeigt dem User immer, was gerade passiert. Wird geladen? Ist ein Fehler aufgetreten? In diesem Tutorial lernst du, wie du Loading- und Error-Zustaende professionell handhabst.
Loading-Zustaende
Einfacher Loading-Spinner
function Spinner() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '2rem'
}}>
<div style={{
width: '40px',
height: '40px',
border: '4px solid #e5e7eb',
borderTop: '4px solid #3498db',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite'
}} />
<style>{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
Skeleton-Loader
Skeleton-Loader zeigen die Form des erwarteten Inhalts – das fuehlt sich schneller an als ein Spinner:
function SkeletonCard() {
const shimmer = {
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
borderRadius: '4px'
};
return (
<div style={{
border: '1px solid #eee',
borderRadius: '12px',
padding: '1.5rem',
maxWidth: '350px'
}}>
<div style={{ ...shimmer, height: '200px', marginBottom: '1rem', borderRadius: '8px' }} />
<div style={{ ...shimmer, height: '24px', width: '70%', marginBottom: '0.5rem' }} />
<div style={{ ...shimmer, height: '16px', width: '100%', marginBottom: '0.25rem' }} />
<div style={{ ...shimmer, height: '16px', width: '85%', marginBottom: '1rem' }} />
<div style={{ ...shimmer, height: '36px', width: '120px' }} />
<style>{`
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
`}</style>
</div>
);
}
// Verwendung
function ProductList() {
const { data: products, loading, error } = useFetch('/api/products');
if (loading) {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
);
}
// Echte Daten anzeigen...
}
Loading-Overlay
Fuer Aktionen, die im Hintergrund laufen:
function LoadingOverlay({ loading, children }) {
return (
<div style={{ position: 'relative' }}>
{children}
{loading && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255,255,255,0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'inherit'
}}>
<Spinner />
</div>
)}
</div>
);
}
Error-Zustaende
Benutzerfreundliche Fehlermeldung
function ErrorMessage({ message, onRetry }) {
return (
<div style={{
padding: '2rem',
textAlign: 'center',
backgroundColor: '#fef2f2',
borderRadius: '12px',
border: '1px solid #fecaca'
}}>
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>!</div>
<h3 style={{ color: '#dc2626', marginBottom: '0.5rem' }}>
Etwas ist schiefgelaufen
</h3>
<p style={{ color: '#666', marginBottom: '1rem' }}>
{message || 'Ein unbekannter Fehler ist aufgetreten.'}
</p>
{onRetry && (
<button
onClick={onRetry}
style={{
padding: '8px 16px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Erneut versuchen
</button>
)}
</div>
);
}
Retry-Logik
function useFetchWithRetry(url, maxRetries = 3) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchData();
}, [url, retryCount]);
function retry() {
if (retryCount < maxRetries) {
setRetryCount(prev => prev + 1);
}
}
return { data, loading, error, retry, retryCount, maxRetries };
}
// Verwendung
function DataView() {
const { data, loading, error, retry, retryCount, maxRetries } =
useFetchWithRetry('/api/data');
if (loading) return <Spinner />;
if (error) {
return (
<ErrorMessage
message={error}
onRetry={retryCount < maxRetries ? retry : undefined}
/>
);
}
return <div>{/* Daten anzeigen */}</div>;
}
Leere Zustaende
Vergiss nicht den Fall, dass die Daten erfolgreich geladen wurden, aber leer sind:
function EmptyState({ title, message, action, onAction }) {
return (
<div style={{
textAlign: 'center',
padding: '3rem',
color: '#666'
}}>
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>📭</div>
<h3 style={{ marginBottom: '0.5rem' }}>{title}</h3>
<p style={{ marginBottom: '1rem' }}>{message}</p>
{action && (
<button
onClick={onAction}
style={{
padding: '8px 16px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}
>
{action}
</button>
)}
</div>
);
}
function TodoList() {
const { data: todos, loading, error } = useFetch('/api/todos');
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!todos || todos.length === 0) {
return (
<EmptyState
title="Keine Todos"
message="Du hast noch keine Todos erstellt."
action="Erstes Todo erstellen"
onAction={() => {/* Todo-Dialog oeffnen */}}
/>
);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Alle Zustaende kombinieren
Ein vollstaendiges Pattern fuer Datenlade-Komponenten:
function UserDashboard() {
const { data: users, loading, error, retry } =
useFetchWithRetry('https://jsonplaceholder.typicode.com/users');
// Loading
if (loading) {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
{[1, 2, 3].map(i => <SkeletonCard key={i} />)}
</div>
);
}
// Error
if (error) {
return <ErrorMessage message={error} onRetry={retry} />;
}
// Empty
if (!users || users.length === 0) {
return <EmptyState title="Keine Benutzer" message="Es sind keine Benutzer vorhanden." />;
}
// Success
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
{users.map(user => (
<div key={user.id} style={{
border: '1px solid #eee',
borderRadius: '12px',
padding: '1.5rem'
}}>
<h3>{user.name}</h3>
<p style={{ color: '#666' }}>{user.email}</p>
<p>{user.company.name}</p>
</div>
))}
</div>
);
}
Inline-Loading fuer Aktionen
Fuer Button-Aktionen wie Speichern oder Loeschen:
function ActionButton({ onClick, children, loadingText = 'Laden...' }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
try {
await onClick();
} finally {
setLoading(false);
}
}
return (
<button
onClick={handleClick}
disabled={loading}
style={{
padding: '8px 16px',
backgroundColor: loading ? '#94a3b8' : '#3498db',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
{loading ? loadingText : children}
</button>
);
}
// Verwendung
<ActionButton
onClick={async () => {
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
}}
loadingText="Wird gespeichert..."
>
Speichern
</ActionButton>
Uebungen
- Erstelle eine Skeleton-Komponente fuer eine Benutzerliste (Avatar, Name, E-Mail)
- Baue eine Fehlerbehandlung mit Retry, die maximal 3 Versuche erlaubt
- Implementiere alle vier Zustaende (Loading, Error, Empty, Success) fuer eine Blog-Post-Liste
Was kommt als Naechstes?
Du weisst jetzt, wie du Loading und Errors professionell handhabst. Im naechsten Kapitel lernst du State Management – wie du den State groesserer Apps effizient organisierst und verwaltst.
Zusammenfassung
- Jeder Datenladevorgang hat vier Zustaende: Loading, Error, Empty, Success
- Skeleton-Loader fuehlen sich schneller an als Spinner
- Biete bei Fehlern immer eine Retry-Moeglichkeit an
- Vergiss nicht den leeren Zustand – zeige hilfreiche Hinweise
- Nutze Loading-Overlays fuer Hintergrund-Aktionen
- Inline-Loading in Buttons gibt visuelles Feedback bei Aktionen
Pro-Tipp: Teste deine Loading- und Error-States gezielt! Nutze die Browser DevTools, um die Netzwerk-Geschwindigkeit zu drosseln (Network Tab → Throttling) oder um Fehler zu simulieren. So siehst du genau, wie sich deine App unter schlechten Bedingungen verhaelt!