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:
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
| Indicator | Use when |
|---|---|
| Text (“Loading…”) | Quick and clear; fine for most small cases |
| Spinner | Short, 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 bar | When you know the percentage (uploads) |
| Disabled button with text | A form being submitted |
A skeleton in the playground:
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.
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
statusstring; start inloading. - Use text, spinners or skeletons to suit the area, and avoid flicker with a short delay.
- Add
aria-busyand a live region for accessibility.