Skip to content

React Tutorial Data and APIs

Loading States in React: Spinners, Skeletons and Status

Lesson 92 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Between “the user asked” and “the data arrived”, the screen needs something to show. A loading state tells the user the app is working. Without it, they see an empty area and wonder whether it is broken.

Track a status, not just a boolean

Beginners use isLoading. A status value is clearer, because loading, success and error exclude each other:

App.jsx
JSX
function fakeApi() {
  return new Promise((resolve) => setTimeout(() => resolve(["Keyboard", "Mouse", "Monitor"]), 700));
}

function App() {
  const [status, setStatus] = React.useState("loading"); // loading | success | error
  const [products, setProducts] = React.useState([]);

  React.useEffect(() => {
    let ignore = false;
    fakeApi().then((data) => {
      if (ignore) return;
      setProducts(data);
      setStatus("success");
    });
    return () => { ignore = true; };
  }, []);

  if (status === "loading") return <p>Loading products...</p>;

  return <ul>{products.map((p) => <li key={p}>{p}</li>)}</ul>;
}

Notice that the loading state starts at the beginning (useState("loading")): the component should not flash an empty list before the effect runs.

Choose the right indicator

IndicatorUse when
Text (“Loading…”)Quick and clear; fine for most small cases
SpinnerShort, unknown waits on a small area (a button, a card)
Skeleton (grey placeholder shapes)Whole sections or lists, because it shows the layout that is coming and reduces jumping
Progress barWhen you know the percentage (uploads)
Disabled button with textA form being submitted

A skeleton in the playground:

App.jsx
JSX
function fakeApi() {
  return new Promise((resolve) => setTimeout(() => resolve(["Learn React in a week", "State made simple", "Hooks without fear"]), 800));
}

function Skeleton() {
  return (
    <div aria-hidden="true">
      <style>{`
        .sk { height: 16px; margin: 10px 0; border-radius: 4px; background: linear-gradient(90deg, #eee 25%, #f6f6f6 37%, #eee 63%); background-size: 400% 100%; animation: shimmer 1.2s ease infinite; }
        @keyframes shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
        @media (prefers-reduced-motion: reduce) { .sk { animation: none; } }
      `}</style>
      <div className="sk" style={{ width: "70%" }} />
      <div className="sk" style={{ width: "55%" }} />
      <div className="sk" style={{ width: "62%" }} />
    </div>
  );
}

function App() {
  const [articles, setArticles] = React.useState(null);

  React.useEffect(() => {
    let ignore = false;
    fakeApi().then((data) => { if (!ignore) setArticles(data); });
    return () => { ignore = true; };
  }, []);

  return (
    <section aria-busy={articles === null}>
      <h3>Latest articles</h3>
      {articles === null ? <Skeleton /> : <ul>{articles.map((a) => <li key={a}>{a}</li>)}</ul>}
    </section>
  );
}

The skeleton reserves the space, so nothing jumps when the content arrives. It also respects prefers-reduced-motion by turning the animation off.

Avoid the flicker of fast requests

If the answer arrives after 50 ms, flashing a spinner for 50 ms looks worse than nothing. Two approaches:

  • Delay the indicator: show it only if loading lasts longer than about 200 to 300 ms.
  • Keep old data visible while new data loads (for search or paging), and show a subtle “updating” hint instead of replacing the content with a spinner.
App.jsx
JSX
function useDelayedFlag(on, delay = 250) {
  const [show, setShow] = useState(false);
  useEffect(() => {
    if (!on) { setShow(false); return; }
    const id = setTimeout(() => setShow(true), delay);
    return () => clearTimeout(id);
  }, [on, delay]);
  return show;
}

Make it accessible

  • Set aria-busy="true" on a region while it loads.
  • Put a polite live region near the content, such as <p role="status">Loading products...</p>, so screen readers hear it.
  • Hide purely decorative placeholders from assistive technology with aria-hidden.
  • Never use only an animation to say “loading”.

Loading each part, not the whole page

Show a loading state where the data will appear, not a full-screen blocker. A header and menu that do not depend on the data should be usable immediately. React’s Suspense makes that pattern simple; see Suspense.

Key points

  • Track a status string; start in loading.
  • Use text, spinners or skeletons to suit the area, and avoid flicker with a short delay.
  • Add aria-busy and a live region for accessibility.