Loading States and UI Feedback During Data Fetching

One of the most crucial aspects of building a great user experience in modern web applications is handling loading states effectively. When data is being fetched from an API, users expect some visual feedback that tells them the app is working — not broken or frozen.

A smooth loading experience not only improves usability but also enhances user trust and engagement. In React, managing loading states is straightforward yet essential for any data-driven interface.

In this post, we’ll explore how to handle loading states, display spinners and skeleton screens, and use conditional rendering to improve the overall user experience. We’ll also discuss design strategies, performance tips, and best practices for handling asynchronous UI updates.


What Are Loading States?

A loading state represents the time period when your app is waiting for a process — such as a network request, file upload, or computation — to complete.

In React, loading states typically occur during data fetching using APIs or asynchronous functions.

For example:

  • Fetching user profiles from a server
  • Loading a product catalog
  • Submitting a form to an API
  • Waiting for authentication

Without proper feedback, the user might think the app has stopped working. That’s why displaying loading indicators like spinners, skeletons, or messages is crucial.


Importance of Loading States

  1. Improves User Trust
    Users stay engaged when they know something is happening in the background.
  2. Prevents Confusion
    A clear indication that data is loading avoids assumptions that the app has crashed.
  3. Enhances Perceived Performance
    Even if actual load time doesn’t change, a smooth loading animation gives the illusion of faster performance.
  4. Improves Accessibility
    Users with slower networks or devices rely on visual feedback for reassurance.

Managing Loading State in React

React provides a flexible way to manage state through hooks such as useState and useEffect.

A typical pattern involves:

  • Setting loading to true before fetching data
  • Setting loading to false once data is fetched
  • Using conditional rendering to display different UI components

Basic Example

import React, { useState, useEffect } from "react";

function App() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
  .then((res) => res.json())
  .then((data) => {
    setData(data);
    setLoading(false);
  });
}, []); return (
<div>
  <h1>Posts</h1>
  {loading ? <p>Loading...</p> : data.slice(0, 5).map((p) => <p key={p.id}>{p.title}</p>)}
</div>
); } export default App;

Explanation

  1. loading starts as true.
  2. When data arrives, it’s set to false.
  3. Conditional rendering displays “Loading…” or the data accordingly.

This simple pattern is the foundation of most loading UI implementations.


Using Conditional Rendering

Conditional rendering means displaying UI based on state. It’s a core React concept.

You can use:

  • Ternary operators
  • Logical && operators
  • Early returns

Example

{loading && <p>Loading...</p>}
{!loading && <p>Data loaded successfully!</p>}

This ensures that only one of the elements appears at a time.


Using a Spinner for Visual Feedback

A spinner is a circular indicator that rotates continuously to show progress.

Example: Basic CSS Spinner

import React, { useState, useEffect } from "react";
import "./App.css";

function SpinnerExample() {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
const timer = setTimeout(() =&gt; setLoading(false), 2000);
return () =&gt; clearTimeout(timer);
}, []); return (
&lt;div&gt;
  &lt;h2&gt;Spinner Loading Example&lt;/h2&gt;
  {loading ? &lt;div className="spinner"&gt;&lt;/div&gt; : &lt;p&gt;Data Loaded!&lt;/p&gt;}
&lt;/div&gt;
); } export default SpinnerExample;

App.css:

.spinner {
  border: 5px solid #f3f3f3;
  border-top: 5px solid #3498db;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 20px auto;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

This example creates a smooth, rotating circle indicating that a process is ongoing.


Skeleton Screens: A Modern Loading Experience

While spinners are helpful, they don’t indicate what content is being loaded. Skeleton screens improve UX by showing placeholder shapes resembling the actual layout.

Example: Basic Skeleton Loader

import React, { useState, useEffect } from "react";
import "./Skeleton.css";

function SkeletonExample() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
setTimeout(() =&gt; {
  setData(&#91;"React", "JavaScript", "Node.js"]);
  setLoading(false);
}, 3000);
}, []); return (
&lt;div&gt;
  &lt;h2&gt;Topics&lt;/h2&gt;
  {loading
    ? &#91;1, 2, 3].map((n) =&gt; &lt;div key={n} className="skeleton-line"&gt;&lt;/div&gt;)
    : data.map((item, index) =&gt; &lt;p key={index}&gt;{item}&lt;/p&gt;)}
&lt;/div&gt;
); } export default SkeletonExample;

Skeleton.css:

.skeleton-line {
  height: 20px;
  width: 200px;
  background-color: #ddd;
  border-radius: 4px;
  margin-bottom: 10px;
  animation: shimmer 1.5s infinite linear;
  background: linear-gradient(90deg, #ddd 25%, #eee 50%, #ddd 75%);
  background-size: 400% 100%;
}

@keyframes shimmer {
  0% { background-position: -400px 0; }
  100% { background-position: 400px 0; }
}

Explanation

  • Displays placeholder bars while data is loading.
  • Animates a “shimmer” effect for modern, smooth feedback.
  • Replaces placeholders with real data when the request completes.

Skeletons are now standard in most professional web apps like Facebook, LinkedIn, and YouTube.


Combining Skeletons and Spinners

In complex applications, you can mix both skeletons and spinners strategically.

  • Use spinners for smaller asynchronous actions (e.g., submitting a form).
  • Use skeleton screens for large content loads (e.g., entire pages).

Example

{loading ? (
  <div>
&lt;div className="skeleton-line"&gt;&lt;/div&gt;
&lt;div className="skeleton-line"&gt;&lt;/div&gt;
</div> ) : ( <ul>
{items.map((item) =&gt; (
  &lt;li key={item.id}&gt;{item.name}&lt;/li&gt;
))}
</ul> )}

This makes the UI feel active and dynamic rather than static.


Delayed Loading Indicators

Sometimes, showing a spinner too quickly can make your app feel slow. If loading takes less than half a second, a flickering spinner might annoy users.

Solution: Delay Display

useEffect(() => {
  const timer = setTimeout(() => setShowSpinner(true), 500);
  fetchData().then(() => {
clearTimeout(timer);
setShowSpinner(false);
}); }, []);

This ensures that the spinner only appears if the operation actually takes some time.


Displaying Progress Bars

For long-running tasks, progress bars provide users with a sense of completion.

Example

import React, { useState, useEffect } from "react";

function ProgressExample() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
const interval = setInterval(() =&gt; {
  setProgress((prev) =&gt; (prev &lt; 100 ? prev + 10 : 100));
}, 500);
return () =&gt; clearInterval(interval);
}, []); return (
&lt;div&gt;
  &lt;h3&gt;Loading Data...&lt;/h3&gt;
  &lt;div style={{ width: "100%", background: "#eee", height: "20px" }}&gt;
    &lt;div
      style={{
        width: ${progress}%,
        height: "100%",
        background: "#3498db",
        transition: "width 0.5s",
      }}
    &gt;&lt;/div&gt;
  &lt;/div&gt;
  {progress === 100 &amp;&amp; &lt;p&gt;Data Loaded!&lt;/p&gt;}
&lt;/div&gt;
); } export default ProgressExample;

This gives users a sense of ongoing progress rather than waiting blindly.


Placeholder Components for Layout Stability

Without placeholders, elements may “jump” when content loads. This phenomenon is known as layout shift, and it hurts user experience.

Example

{loading ? (
  <div style={{ height: "100px", background: "#eee", borderRadius: "8px" }}></div>
) : (
  <img src={imageURL} alt="Loaded" />
)}

Keeping the placeholder height equal to the loaded content prevents layout movement and improves visual stability.


Handling Multiple Loading States

In complex applications, you might have multiple simultaneous loading actions — for example, fetching users and posts.

Example

if (loadingUsers || loadingPosts) {
  return <p>Loading data...</p>;
}

return (
  <div>
&lt;UserList data={users} /&gt;
&lt;PostList data={posts} /&gt;
</div> );

You can also display separate loading indicators for each section.


Error and Empty States

Good UX means not just handling loading states, but also error and empty states gracefully.

Example

if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching data</p>;
if (data.length === 0) return <p>No data available</p>;

This ensures users always have meaningful visual feedback.


Using Third-Party Libraries

Several React libraries provide polished loading components:

  1. React Spinners – For prebuilt CSS-based loaders. npm install react-spinners
  2. React Loading Skeleton – For flexible skeleton placeholders. npm install react-loading-skeleton
  3. React Content Loader – For SVG-based animated placeholders. npm install react-content-loader

Example using React Loading Skeleton:

import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";

function UserCard({ loading, user }) {
  return (
&lt;div&gt;
  {loading ? (
    &lt;Skeleton height={50} count={3} /&gt;
  ) : (
    &lt;div&gt;
      &lt;h3&gt;{user.name}&lt;/h3&gt;
      &lt;p&gt;{user.email}&lt;/p&gt;
    &lt;/div&gt;
  )}
&lt;/div&gt;
); }

This approach saves time and gives your app a professional touch.


Real-Life Example: Full Loading Workflow

import React, { useState, useEffect } from "react";

function Dashboard() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
const fetchDashboard = async () =&gt; {
  try {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts");
    if (!res.ok) throw new Error("Failed to fetch data");
    const json = await res.json();
    setData(json);
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
};
fetchDashboard();
}, []); if (loading) return <p>Loading dashboard...</p>; if (error) return <p>Error: {error}</p>; return (
&lt;div&gt;
  &lt;h2&gt;Dashboard&lt;/h2&gt;
  {data.slice(0, 5).map((item) =&gt; (
    &lt;div key={item.id}&gt;
      &lt;h4&gt;{item.title}&lt;/h4&gt;
      &lt;p&gt;{item.body}&lt;/p&gt;
    &lt;/div&gt;
  ))}
&lt;/div&gt;
); } export default Dashboard;

This workflow represents real-world loading management: initial loading, success, and error rendering.


Design Tips for Loading UI

  1. Show context, not just motion – Skeletons give users an idea of what’s loading.
  2. Keep feedback consistent – Use similar loading indicators throughout your app.
  3. Use motion sparingly – Avoid excessive animations that can distract users.
  4. Keep the interface interactive – Allow users to cancel or retry operations.
  5. Avoid blank screens – Always show at least some feedback.
  6. Anticipate data length – Use skeleton counts that reflect expected data volume.

Accessibility Considerations

Accessibility is vital when designing loading indicators.

  1. Provide ARIA roles: <div role="status" aria-live="polite">Loading...</div> This helps screen readers announce loading progress.
  2. Avoid infinite loaders:
    Always resolve loading states or provide timeout messages.
  3. Color contrast:
    Make sure spinners and skeletons are visible in both light and dark themes.

Optimizing Perceived Performance

Even if your app’s load time is constant, you can make it feel faster through smart UI patterns:

  1. Prefetch data: Fetch before the user needs it.
  2. Cache API responses: Save results locally to avoid unnecessary reloading.
  3. Use optimistic updates: Temporarily show data before confirmation.
  4. Load progressively: Display partial data while the rest loads.

Best Practices Summary

GoalBest Practice
Communicate progressAlways show a visual indicator
Prevent confusionUse skeletons for content-heavy pages
Handle all outcomesInclude loading, error, and empty states
Maintain layoutReserve space for loaded elements
Keep users engagedUse animation, but avoid overuse
Improve accessibilityUse ARIA attributes and visible colors


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *