Loading & Error States

Track loading, success and error so the UI always has something to show.

Real data fetching has three outcomes: it is loading, it succeeded, or it failed. Track all three so the user is never staring at a blank screen.

A reliable shape

  • loading — show a spinner or skeleton.
  • error — show a message and maybe a retry button.
  • data — render the content.

Example

Example · javascript
import { useState, useEffect } from 'react';

function Posts() {
  const [state, setState] = useState({ loading: true, error: null, data: [] });
  useEffect(() => {
    fetch('/api/posts')
      .then((r) => {
        if (!r.ok) throw new Error('Failed to load');
        return r.json();
      })
      .then((data) => setState({ loading: false, error: null, data }))
      .catch((err) => setState({ loading: false, error: err.message, data: [] }));
  }, []);
  if (state.loading) return <p>Loading...</p>;
  if (state.error) return <p>{state.error}</p>;
  return <ul>{state.data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

When to use it

  • A product page shows a skeleton card while the product data loads and an error banner with a Retry button if the network request fails.
  • A dashboard widget tracks three independent states — loading, data, and error — and renders the appropriate UI for each without nesting ternaries.
  • A form submit handler enters a loading state, disables the submit button during the request, and switches to an error message if the server returns a non-200 status.

More examples

Three-state data fetch

Tracks loading, data, and error in separate state variables and renders the correct UI for each outcome.

Example · jsx
import { useState, useEffect } from 'react';

function ArticleView({ id }) {
  const [article, setArticle] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/articles/${id}`)
      .then(r => { if (!r.ok) throw new Error('Not found'); return r.json(); })
      .then(setArticle)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [id]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p className="error">{error.message}</p>;
  return <article><h1>{article.title}</h1></article>;
}

Retry button on error

Provides a Retry button that re-triggers the fetch, resetting the error state before the new request starts.

Example · jsx
import { useState, useCallback } from 'react';

function DataWidget({ url }) {
  const [state, setState] = useState({ data: null, loading: false, error: null });

  const load = useCallback(() => {
    setState(s => ({ ...s, loading: true, error: null }));
    fetch(url)
      .then(r => r.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => setState({ data: null, loading: false, error: err.message }));
  }, [url]);

  if (state.loading) return <p>Loading...</p>;
  if (state.error) return <>
    <p className="error">{state.error}</p>
    <button onClick={load}>Retry</button>
  </>;
  return state.data ? <pre>{JSON.stringify(state.data)}</pre> : <button onClick={load}>Load</button>;
}

Combined state object pattern

Replaces multiple useState calls with a reducer to manage status, data, and error as a single coherent state machine.

Example · jsx
import { useReducer, useEffect } from 'react';

const init = { status: 'idle', data: null, error: null };
function reducer(state, action) {
  switch (action.type) {
    case 'LOADING': return { status: 'loading', data: null, error: null };
    case 'SUCCESS': return { status: 'success', data: action.data, error: null };
    case 'ERROR':   return { status: 'error', data: null, error: action.error };
    default: return state;
  }
}

function RemoteData({ url }) {
  const [state, dispatch] = useReducer(reducer, init);
  useEffect(() => {
    dispatch({ type: 'LOADING' });
    fetch(url).then(r => r.json())
      .then(data => dispatch({ type: 'SUCCESS', data }))
      .catch(err => dispatch({ type: 'ERROR', error: err.message }));
  }, [url]);
  return <p>{state.status}</p>;
}

Discussion

  • Be the first to comment on this lesson.