Waarom Loading States & Error Handling?

Wanneer je data ophaalt van een API, duurt het even voordat de data er is. Gebruikers willen feedback krijgen over wat er gebeurt.

Slechte user experience (zonder loading):

  • Lege pagina zonder uitleg
  • Gebruiker weet niet of het werkt
  • Crashes als er iets mis gaat

Goede user experience (met loading):

  • "Loading..." bericht tijdens laden
  • Duidelijke foutmeldingen als iets mis gaat
  • Professionele uitstraling

Het 3 States Pattern

Voor elke data fetch heb je meestal 3 states nodig:

const [data, setData] = useState([]);        // De data zelf
const [loading, setLoading] = useState(true); // Is het aan het laden?
const [error, setError] = useState(null);     // Is er een fout?

Flow:

  1. loading = true → Toon "Loading..."
  2. Fetch data van API
  3. Success? → loading = false, data = result
  4. Error? → loading = false, error = message

Volledig Voorbeeld

import { useState, useEffect } from 'react';

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);  // Start loading
        setError(null);    // Reset error
        
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users'
        );
        
        if (!response.ok) {
          throw new Error('Er is iets misgegaan');
        }
        
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);  // Stop loading (altijd)
      }
    };

    fetchUsers();
  }, []);

  // Loading state
  if (loading) {
    return <p>Loading users...</p>;
  }

  // Error state
  if (error) {
    return <p style={{ color: 'red' }}>Error: {error}</p>;
  }

  // Success state
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

export default UserList;

Stappen uitleg:

  1. try - Probeer data te fetchen
  2. catch - Vang fouten op en zet in state
  3. finally - Stop loading (gebeurt altijd!)
  4. Render based op state: loading → error → success

Andere Patterns

Inline Loading & Error

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // ... fetch logic

  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      {!loading && !error && (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
};

Loading Spinner Component

const Spinner = () => (
  <div style={{ textAlign: 'center', padding: '20px' }}>
    <div className="spinner"></div>
    <p>Loading...</p>
  </div>
);

// Gebruik:
if (loading) return <Spinner />;

Error Component

const ErrorMessage = ({ message, onRetry }) => (
  <div style={{ color: 'red', padding: '20px' }}>
    <h3>Oeps! Er ging iets mis</h3>
    <p>{message}</p>
    {onRetry && (
      <button onClick={onRetry}>Probeer opnieuw</button>
    )}
  </div>
);

// Gebruik:
if (error) {
  return <ErrorMessage message={error} onRetry={fetchUsers} />;
}

Met Retry Functie

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchUsers = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch('https://api.example.com/users');
      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  if (loading) return <p>Loading...</p>;
  
  if (error) {
    return (
      <div>
        <p>Error: {error}</p>
        <button onClick={fetchUsers}>Probeer opnieuw</button>
      </div>
    );
  }

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

Tips & Best Practices

  • Altijd finally gebruiken - Zo wordt loading altijd gestopt
  • Reset error bij nieuwe fetch - Voorkom oude foutmeldingen
  • Specifieke foutmeldingen - "Network error" ipv "Something went wrong"
  • Retry optie bij errors - Geef gebruiker de mogelijkheid het opnieuw te proberen
  • Loading op false starten - Als data al lokaal beschikbaar is

Veelgemaakte Fouten

Fout - Loading niet stoppen bij error:

try {
  const data = await fetch(url);
  setLoading(false);
} catch (err) {
  setError(err.message);
  // Loading blijft true!
}

Goed - Gebruik finally:

try {
  const data = await fetch(url);
} catch (err) {
  setError(err.message);
} finally {
  setLoading(false); // Altijd uitgevoerd
}

Fout - Geen error handling:

const data = await fetch(url);
// Als fetch faalt, crasht de app!

Goed - Altijd try/catch:

try {
  const data = await fetch(url);
} catch (err) {
  setError(err.message);
}