We use cookies to enhance your experience on the site
CodeWorlds

Loading, Error, and Empty Data States

When our spaceship sends a request to Mission Control, several things can happen: the data is on its way (loading), the transmission failed (error), or the center has no data to send (empty result). We must handle each of these situations!

The Loading/Error/Data Pattern

The most popular pattern in React for handling data fetching uses three state variables:

1import React, { useState, useEffect } from 'react';
2
3function StarshipCatalog() {
4  const [data, setData] = useState(null);
5  const [loading, setLoading] = useState(true);
6  const [error, setError] = useState(null);
7
8  useEffect(() => {
9    async function fetchStarships() {
10      try {
11        setLoading(true);
12        setError(null);
13
14        const response = await fetch(
15          'https://swapi.dev/api/starships/'
16        );
17
18        if (!response.ok) {
19          throw new Error(\`HTTP Error: \${response.status}\`);
20        }
21
22        const result = await response.json();
23        setData(result.results);
24      } catch (err) {
25        setError(err.message);
26      } finally {
27        setLoading(false);
28      }
29    }
30
31    fetchStarships();
32  }, []);
33
34  // Loading state
35  if (loading) {
36    return <div className="loading">Scanning the sector...</div>;
37  }
38
39  // Error state
40  if (error) {
41    return <div className="error">Communication error: {error}</div>;
42  }
43
44  // Empty data state
45  if (!data || data.length === 0) {
46    return <div className="empty">No ships found in this sector</div>;
47  }
48
49  // Data ready to display
50  return (
51    <div>
52      <h2>Ships detected: {data.length}</h2>
53      {data.map(ship => (
54        <div key={ship.name}>
55          <h3>{ship.name}</h3>
56          <p>Model: {ship.model}</p>
57        </div>
58      ))}
59    </div>
60  );
61}

Conditional State Rendering

We can create nicer components for each state:

1function LoadingSpinner() {
2  return (
3    <div className="spinner-container">
4      <div className="spinner"></div>
5      <p>Loading data from mission control...</p>
6    </div>
7  );
8}
9
10function ErrorMessage({ message, onRetry }) {
11  return (
12    <div className="error-container">
13      <h3>Connection lost!</h3>
14      <p>{message}</p>
15      <button onClick={onRetry}>
16        Retry
17      </button>
18    </div>
19  );
20}
21
22function EmptyState() {
23  return (
24    <div className="empty-container">
25      <p>No data in this galactic quadrant</p>
26      <p>Try scanning a different sector</p>
27    </div>
28  );
29}

Retry Mechanism

When communication with the server fails, it's worth giving the user the option to retry:

1function SpaceData() {
2  const [data, setData] = useState(null);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  const fetchData = async () => {
7    try {
8      setLoading(true);
9      setError(null);
10      const response = await fetch(
11        'https://swapi.dev/api/people/'
12      );
13      if (!response.ok) throw new Error('Network error');
14      const result = await response.json();
15      setData(result.results);
16    } catch (err) {
17      setError(err.message);
18    } finally {
19      setLoading(false);
20    }
21  };
22
23  useEffect(() => {
24    fetchData();
25  }, []);
26
27  if (loading) return <LoadingSpinner />;
28  if (error) return <ErrorMessage message={error} onRetry={fetchData} />;
29  if (!data || data.length === 0) return <EmptyState />;
30
31  return (
32    <ul>
33      {data.map(person => (
34        <li key={person.name}>{person.name}</li>
35      ))}
36    </ul>
37  );
38}

The key difference: the

fetchData
function is defined outside
useEffect
, so we can call it again from the "Retry" button.

Skeleton Loading

Instead of a simple spinner, you can display an interface skeleton:

1function SkeletonCard() {
2  return (
3    <div className="skeleton-card">
4      <div className="skeleton-title"></div>
5      <div className="skeleton-text"></div>
6      <div className="skeleton-text short"></div>
7    </div>
8  );
9}
10
11function PlanetCatalog() {
12  const [planets, setPlanets] = useState(null);
13  const [loading, setLoading] = useState(true);
14
15  // ... fetch logic ...
16
17  if (loading) {
18    return (
19      <div>
20        <SkeletonCard />
21        <SkeletonCard />
22        <SkeletonCard />
23      </div>
24    );
25  }
26
27  return (
28    <div>
29      {planets.map(planet => (
30        <PlanetCard key={planet.name} planet={planet} />
31      ))}
32    </div>
33  );
34}

Remember -- each state (loading, error, success, empty) requires dedicated UI. Good state handling is the difference between a professional application and an amateur project. The user should never stare at a blank screen without information about what's happening!

Go to CodeWorlds