Q&A
How to create a loading spinner in React
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:
- react-spinners - drop-in components like
<ClipLoader loading={loading} />. - Material UI -
<CircularProgress />. - lucide-react - animate the
Loadericon with a Tailwind class:<Loader className="animate-spin" />.
Modern Patterns
- Suspense: React's
<Suspense fallback={<Spinner />}>shows a fallback while a lazy component or data source loads. - Skeleton screens: instead of a spinner, render gray placeholder shapes - often better perceived performance for content-heavy UIs.
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
- Forgetting to set
loading = falseon error → spinner spins forever. - Setting state after the component unmounts (no cleanup) → React warning / leak.
- Animating with JavaScript instead of CSS (CSS animations are smoother and cheaper).
- No accessibility attributes, leaving screen-reader users unaware of loading.
References
- React
useEffect: https://react.dev/reference/react/useEffect - MDN CSS animations: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations