Daten laden mit fetch
Lerne, wie du in React Daten von APIs lädst. Fetch API, async/await und Daten in Komponenten integrieren.
Daten laden mit fetch
Kaum eine React-App kommt ohne Daten von einem Server aus. Ob Benutzerlisten, Blogposts oder Produktdaten – du musst wissen, wie du APIs anbindest. In diesem Tutorial lernst du alles ueber das Laden von Daten in React.
Grundprinzip: Daten in useEffect laden
Das Standardmuster fuer Datenladen in React:
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error(`HTTP-Fehler: ${response.status}`);
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) return <p>Laden...</p>;
if (error) return <p>Fehler: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} – {user.email}</li>
))}
</ul>
);
}
Die drei Zustaende
Jeder Datenladevorgang hat drei Zustaende:
| Zustand | Beschreibung | UI |
|---|---|---|
| Loading | Daten werden geladen | Lade-Indikator |
| Success | Daten erfolgreich geladen | Daten anzeigen |
| Error | Fehler aufgetreten | Fehlermeldung |
function DataDisplay({ url }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setData(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [url]);
if (loading) return <div className="skeleton">Laden...</div>;
if (error) return <div className="error">Fehler: {error}</div>;
if (!data) return <div>Keine Daten.</div>;
return <div>{/* Daten anzeigen */}</div>;
}
Custom Hook: useFetch
Extrahiere die Lade-Logik in einen wiederverwendbaren Hook:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) {
setLoading(false);
return;
}
const controller = new AbortController();
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// So einfach wird die Verwendung:
function PostList() {
const { data: posts, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/posts'
);
if (loading) return <p>Laden...</p>;
if (error) return <p>Fehler: {error}</p>;
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
POST-Requests: Daten senden
function CreatePost() {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [submitting, setSubmitting] = useState(false);
const [result, setResult] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, body, userId: 1 })
});
if (!response.ok) throw new Error('Erstellen fehlgeschlagen');
const data = await response.json();
setResult(data);
setTitle('');
setBody('');
} catch (err) {
alert('Fehler: ' + err.message);
} finally {
setSubmitting(false);
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Titel"
required
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Inhalt"
rows={5}
required
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Wird erstellt...' : 'Post erstellen'}
</button>
</form>
{result && (
<div style={{ marginTop: '1rem', padding: '1rem', background: '#d4edda', borderRadius: '8px' }}>
Post erstellt mit ID: {result.id}
</div>
)}
</div>
);
}
Daten abhaengig von Parametern laden
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const { data: posts } = useFetch(
user ? `https://jsonplaceholder.typicode.com/posts?userId=${userId}` : null
);
if (loading) return <p>Profil wird geladen...</p>;
if (error) return <p>Fehler: {error}</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<h2>Posts von {user.name}</h2>
{posts ? (
posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
</article>
))
) : (
<p>Posts werden geladen...</p>
)}
</div>
);
}
Paginierung
function PaginatedPosts() {
const [page, setPage] = useState(1);
const limit = 10;
const { data: posts, loading } = useFetch(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${limit}`
);
return (
<div>
<h1>Blog-Posts</h1>
{loading ? (
<p>Laden...</p>
) : (
posts?.map(post => (
<article key={post.id} style={{
padding: '1rem',
borderBottom: '1px solid #eee'
}}>
<h2>{post.title}</h2>
<p>{post.body.substring(0, 100)}...</p>
</article>
))
)}
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1 || loading}
>
Vorherige
</button>
<span>Seite {page}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={loading || (posts && posts.length < limit)}
>
Naechste
</button>
</div>
</div>
);
}
Suche mit Debounce
function SearchPosts() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
const { data: posts, loading } = useFetch(
debouncedQuery
? `https://jsonplaceholder.typicode.com/posts?q=${debouncedQuery}`
: null
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Posts suchen..."
style={{ padding: '0.5rem', width: '100%', fontSize: '1rem' }}
/>
{loading && <p>Suche...</p>}
{posts && (
<div>
<p>{posts.length} Ergebnis(se)</p>
{posts.map(post => (
<div key={post.id} style={{ padding: '0.5rem 0', borderBottom: '1px solid #eee' }}>
<strong>{post.title}</strong>
</div>
))}
</div>
)}
</div>
);
}
Uebungen
- Baue eine User-Liste, die Daten von JSONPlaceholder laedt und anzeigt
- Erstelle eine Detail-Ansicht, die beim Klick auf einen User dessen Posts laedt
- Implementiere eine Suche mit Debounce, die Posts nach Titel filtert
Was kommt als Naechstes?
Du weisst jetzt, wie du Daten ladest. Im naechsten Kapitel lernst du Loading- und Error-States richtig zu behandeln – mit Skeleton-Loadern, Retry-Logik und benutzerfreundlichen Fehlermeldungen.
Zusammenfassung
- Lade Daten in
useEffectmitasync/awaitundfetch - Verwalte immer die drei Zustaende: Loading, Success und Error
- Nutze
AbortControllerum veraltete Requests abzubrechen - Extrahiere die Lade-Logik in einen
useFetchCustom Hook - Fuer Paginierung nutze Page-Parameter in der URL
- Fuer Suche nutze Debounce, um zu viele Requests zu vermeiden
Pro-Tipp: In Produktions-Apps solltest du eine Bibliothek wie TanStack Query (React Query) verwenden. Sie bietet Caching, automatische Refetches, Hintergrund-Updates und vieles mehr – alles Dinge, die du sonst selbst implementieren muessstest. Aber das manuelle Laden mit fetch zu verstehen, ist die beste Grundlage dafuer!