M4RKYU.SYSEdition 2027
Skip to content
LOCEN/Ontario · CA/▸logs · 3 ways to create engaging react loading screens with hooks 37b1StandbyOK/--:--:--EST
M4M4RK_YUportfolio
  • BuildBuild
    BuildOverview
    • WorkSelected case studies and write-ups
    • GamesPlayable prototypes and game-dev logs
  • GalleryGallery
    GalleryOverview
    • PhotosPhoto collections and visual experiments
    • ShopPrints, posters, and one-off objects
  • WritingWriting
    WritingOverview
    • BlogLong-form devlogs and field notes
    • NotesShort observations, links, snippets
  • ResourcesResources
    ResourcesOverview
    • Tools38 in-browser developer utilities
    • LinksDaily-use dev and design bookmarks
  • AboutAbout
  • ContactContact
中文

syndicated · dev.to / @markyu

React Loading Screens Are a State Machine Problem

A practical React loading screen guide using hooks, request states, error states, CSS animation, and why spinners alone are not enough.

Published
May 3 '24
·
Reading time
2 min read
·
Reactions
7
reactfrontendjavascriptux
View on dev.to

On this page

  • The State Shape I Prefer
  • A Practical Fetch Component
  • CSS Spinner Without a Library
  • When Lottie Makes Sense
  • My Rule for Loading UI
  • Final Thought

A loading screen is not a spinner problem.

It is a state problem.

The UI needs to know whether the request is idle, loading, successful, empty, or failed. If you only track loading: true/false, your component will eventually lie to the user.

The State Shape I Prefer

const [state, setState] = useState({
  status: "idle",
  data: null,
  error: null,
});

The useful statuses:

idle -> loading -> success
               \-> empty
               \-> error

This is already better than done.

A Practical Fetch Component

import { useEffect, useState } from "react";

export default function Posts() {
  const [state, setState] = useState({
    status: "idle",
    data: [],
    error: null,
  });

  useEffect(() => {
    let ignore = false;

    async function loadPosts() {
      setState({ status: "loading", data: [], error: null });

      try {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts");
        if (!res.ok) throw new Error(`HTTP ${res.status}`);

        const data = await res.json();
        if (ignore) return;

        setState({
          status: data.length ? "success" : "empty",
          data,
          error: null,
        });
      } catch (error) {
        if (!ignore) {
          setState({ status: "error", data: [], error });
        }
      }
    }

    loadPosts();

    return () => {
      ignore = true;
    };
  }, []);

  if (state.status === "loading") return <LoadingScreen />;
  if (state.status === "error") return <ErrorMessage error={state.error} />;
  if (state.status === "empty") return <p>No posts yet.</p>;

  return (
    <ul>
      {state.data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

The ignore flag avoids setting state after the component unmounts. It is a small detail, but it prevents a class of annoying bugs.

CSS Spinner Without a Library

function LoadingScreen() {
  return (
    <div className="loader" role="status" aria-live="polite">
      <span className="spinner" />
      <p>Loading posts...</p>
    </div>
  );
}
.loader {
  min-height: 180px;
  display: grid;
  place-items: center;
  gap: 12px;
}

.spinner {
  width: 36px;
  height: 36px;
  border: 4px solid #e5e7eb;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
  }
}

I prefer this over adding a spinner dependency for a simple app.

When Lottie Makes Sense

Lottie is useful when loading is part of the product experience: onboarding, file upload, payment confirmation, or a long-running AI task.

I would not use it for every API fetch.

If the request usually takes 200ms, a dramatic animation feels slower, not better.

My Rule for Loading UI

Load timeUI
< 300msavoid spinner if possible
300ms - 2ssmall inline loader
2s - 10sskeleton or progress context
> 10sprogress, cancellation, or background job

For AI-agent and long-running 2026 workflows, the last row matters a lot. Users need to know whether the system is still thinking, stuck, or waiting on a tool.

Final Thought

A good loading screen tells the truth.

It should not just spin. It should represent the real state of the request and give the user enough confidence to wait or recover.

How do you handle long-running loading states in your React apps?

Related reading

React 19 Micro-Interactions Without Layout Jank

A practical React 19 micro-interactions guide focused on motion boundaries, CSS transitions, optimistic UI, reduced motion, and performance.

react

CSS Heart Animation: Small Demo, Real Animation Lessons

A cleaner CSS heart animation tutorial focused on transform, pseudo-elements, keyframes, and the small mistakes that break simple UI animations.

css

Frontend Linear Data Structures Deep Dive: Arrays, Stacks, Queues, and Linked Lists

The Big Picture Before diving into stacks, queues, and linked lists, it helps to know...

computerscience

originally published

This post first ran on dev.to. Comments and reactions live there.

Continue on dev.to
PreviousA* Search Finally Clicked When I Drew the GridA practical developer-friendly explanation of heuristic search, Greedy Best-First Search, and A* using a grid example instead of textbook language.
Back to all posts
NextDocker Containers: The Commands That Prove IsolationA practical Docker container guide focused on the commands that show image layers, process isolation, networking, volumes, and debugging.
Back to archive
M4RKYUM4RKYUM4RKYUM4RKYUM4RKYUM4RKYUM4RKYUM4RKYU
Crafted since 2024
ZhenXiao Mark YuZhenXiao Mark Yu
get in touch

Saw something here?Tell me about it.

It's a portfolio, not a service · but I read every note — drop a line if anything here resonated, or just to say hi.

Start a conversation
open channel

say hi anytime · 2026

--:--:--ESTOntario, Canada
  • Email
  • GitHub
  • dev.to
  • LinkedIn
  • Twitter / X
  • Instagram
  • Facebook
  • YouTube
  • CodePen
  • Spotify
  • Snapchat

Newsletter

Get the occasional dispatch

Notes and logs from m4rkyu.com — short, dated, no noise. Unsubscribe anytime.

Work

Production builds, games, and visual archives.

  • Projects
  • Games
  • Archive
  • Logs

Resources

Daily-use tools and a personal link library.

  • Search
  • Latest
  • Tools
  • Links
  • Notes
  • Topics
  • Shop
RSSJSON feed

Studio

Background, contact, and channels for collaboration.

  • About
  • Contact
  • Changelog
  • Colophon
  • Resumepending

Socials

Find me on the usual feeds.

  • GitHub
  • dev.to
  • LinkedIn
  • Twitter / X
  • Instagram
  • Facebook
  • YouTube
  • CodePen
  • Spotify
  • Snapchat
  • Email
© 2026 ZhenXiao Mark Yumarkyu0615@gmail.com
  • Email
  • GitHub
  • dev.to
  • LinkedIn
  • Twitter / X
  • Instagram
  • Facebook
  • YouTube
  • CodePen
  • Spotify
  • Snapchat
PrivacyTermsBuilt with Next.js 16 · React 19 · Tailwind 4