> ## Documentation Index
> Fetch the complete documentation index at: https://js.maxbraglia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Loading states

> Show users what's happening while data is being fetched

## Why loading states matter

When your app fetches data, there's a gap between clicking and seeing results. Without a loading state, users see either nothing (blank screen) or stale data. They don't know if the app is working.

```jsx theme={null}
// ❌ No loading state — user sees blank page for 1-3 seconds
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("/api/users")
      .then(r => r.json())
      .then(data => setUsers(data));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
  // Empty list until data arrives — looks broken
}
```

## The loading state pattern

Every data-fetching component needs three states: **loading**, **error**, and **data**.

```jsx theme={null}
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadUsers() {
      try {
        setLoading(true);
        const response = await fetch("/api/users");
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;
  if (users.length === 0) return <p>No users found.</p>;

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

This is the pattern you'll use in almost every component that fetches data. Three states, three checks at the top of the return.

<Info>
  Notice the order: check `loading` first, then `error`, then empty state, then render data. This order matters because you want to show the most relevant state.
</Info>

## Loading indicators

### Simple text

```jsx theme={null}
if (loading) return <p>Loading...</p>;
```

### Spinner component

```jsx theme={null}
function Spinner() {
  return <div className="spinner" aria-label="Loading" />;
}

// Usage
if (loading) return <Spinner />;
```

```css theme={null}
.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #e5e7eb;
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
```

### Inline loading (inside existing content)

```jsx theme={null}
function UserList() {
  // ...state setup

  return (
    <div>
      <h2>Users {loading && <Spinner />}</h2>
      {error && <p className="error">{error}</p>}
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
```

<Tip>
  Use full-page loading for initial loads and inline loading for refreshes. If the user already sees data, don't replace it with a spinner — show the spinner alongside the existing content.
</Tip>

## Disabling buttons during requests

Prevent users from double-clicking submit buttons:

```jsx theme={null}
function CreateUserForm() {
  const [name, setName] = useState("");
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setSubmitting(true);

    try {
      const response = await fetch("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name }),
      });

      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const newUser = await response.json();
      console.log("Created:", newUser);
    } catch (error) {
      console.error("Failed:", error);
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        disabled={submitting}
      />
      <button type="submit" disabled={submitting}>
        {submitting ? "Creating..." : "Create User"}
      </button>
    </form>
  );
}
```

Key details:

* `disabled={submitting}` prevents the button from being clicked again
* Button text changes to show progress ("Creating...")
* Input is also disabled to prevent editing during submission
* `finally` ensures the button re-enables even if the request fails

<Warning>
  Always disable submit buttons during requests. Without this, users can click multiple times and create duplicate entries. This is one of the most common bugs in web applications.
</Warning>

## The complete data fetching pattern

This is the pattern you'll reuse across your entire application:

```jsx theme={null}
function DataComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch("/api/endpoint");
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!data) return <EmptyState />;

  return <DataDisplay data={data} />;
}
```

<Tip>
  This three-state pattern (loading / error / data) shows up so often that libraries like React Query and SWR were created to automate it. For now, writing it manually teaches you what's happening under the hood.
</Tip>

## Refactoring into a custom hook

Once you're comfortable with the pattern, extract it into a reusable hook:

```jsx theme={null}
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        setData(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage — so clean!
function UserList() {
  const { data: users, loading, error } = useFetch("/api/users");

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

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

One hook, any endpoint. The same loading/error/data pattern without repeating yourself.

## What's next?

You've covered the complete async & APIs section — from understanding async JavaScript to making real API calls with proper error handling and loading states.

Next up: the DOM and browser APIs. You'll learn how JavaScript interacts with the page itself — selecting elements, modifying content, and responding to user actions.

<Card title="What is the DOM?" icon="sitemap" href="/dom-browser/what-is-dom">
  Understand how JavaScript sees and interacts with your page
</Card>
