AlgoInsight

Q&A

How to create a loading spinner in React

By AlgoInsight Team2026-06-07concurrencyfrontendreactweb-developmentcssjavascript

Overview

A spinner communicates that work is in progress. In React this is a state-driven UI problem: the spinner's visibility is derived from a piece of loading state, and the animation is pure CSS. The pattern generalizes to any async work - data fetching, form submission, navigation.

graph TD
    A[loading state = false] --> B[Start async op: setLoading(true)]
    B --> C[Render spinner while loading]
    C --> D[Op finishes: setLoading(false)]
    D --> E[Render data or error]
    style C fill:#1e1b4b,color:#fff

Step 1: The Spinner Component (CSS animation)

// Spinner.jsx
export default function Spinner() {
  return <div className="spinner" role="status" aria-label="Loading" />;
}
/* a rotating border circle */
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e5e7eb;        /* track */
  border-top-color: #6366f1;        /* moving arc */
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

The role="status" and aria-label make it accessible to screen readers.

Step 2: Drive It with Loading State

import { useState, useEffect } from "react";
import Spinner from "./Spinner";

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

  useEffect(() => {
    let active = true;
    setLoading(true);
    fetch("/api/users")
      .then(res => {
        if (!res.ok) throw new Error("Request failed");
        return res.json();
      })
      .then(data => { if (active) setUsers(data); })
      .catch(err => { if (active) setError(err.message); })
      .finally(() => { if (active) setLoading(false); });
    return () => { active = false; };   // avoid setting state after unmount
  }, []);

  if (loading) return <Spinner />;
  if (error)   return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

The three-way render - spinner / error / data - is the idiomatic structure. The finally guarantees the spinner clears whether the call succeeds or fails.

Step 3 (optional): Inline Button Spinner

For form submissions, show the spinner inside the button and disable it:

<button onClick={submit} disabled={submitting}>
  {submitting ? <Spinner /> : "Save"}
</button>

Using a Library Instead

You can skip the CSS with libraries:

Modern Patterns

Why This Matters in Interviews

Front-end interviews use this to check state-driven rendering, async handling in useEffect, and conditional rendering (loading/error/data). The strong-candidate details: clearing the spinner in finally (so it never hangs on error), the cleanup flag to avoid setting state after unmount, accessibility (role="status"), and awareness of Suspense/skeletons as alternatives.

Common Pitfalls

References

More answers