Zum Inhalt springen
React Anfänger 30 min

Daten laden mit fetch

Lerne, wie du in React Daten von APIs lädst. Fetch API, async/await und Daten in Komponenten integrieren.

Aktualisiert:

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:

ZustandBeschreibungUI
LoadingDaten werden geladenLade-Indikator
SuccessDaten erfolgreich geladenDaten anzeigen
ErrorFehler aufgetretenFehlermeldung
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

  1. Baue eine User-Liste, die Daten von JSONPlaceholder laedt und anzeigt
  2. Erstelle eine Detail-Ansicht, die beim Klick auf einen User dessen Posts laedt
  3. 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 useEffect mit async/await und fetch
  • Verwalte immer die drei Zustaende: Loading, Success und Error
  • Nutze AbortController um veraltete Requests abzubrechen
  • Extrahiere die Lade-Logik in einen useFetch Custom 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!

Zurück zum React Kurs