We use cookies to enhance your experience on the site
CodeWorlds

Loading States and Skeletons - UX for Data Loading

Users hate waiting without feedback. Loading states and skeleton loaders improve UX by showing that something is happening.

What are Loading States? ⏳

A loading state is the application state while data is being loaded (e.g., from an API).

Basic example:

1function PirateFeed() {
2  const [posts, setPosts] = useState<Post[]>([]);
3  const [loading, setLoading] = useState(true);
4
5  useEffect(() => {
6    fetchPosts()
7      .then(data => setPosts(data))
8      .finally(() => setLoading(false));
9  }, []);
10
11  if (loading) {
12    return <div className="text-center p-8">Loading...</div>;
13  }
14
15  return (
16    <div>
17      {posts.map(post => <PostCard key={post.id} post={post} />)}
18    </div>
19  );
20}

Problem: "Loading..." is boring. Users don't know how long they need to wait.

Skeleton Loaders - Better Feedback 💀

A skeleton is a placeholder that mimics the layout of the content being loaded.

Example - Skeleton for PostCard:

1function PostSkeleton() {
2  return (
3    <div className="border rounded-lg p-6 bg-white animate-pulse">
4      {/* Header */}
5      <div className="flex items-center gap-3 mb-4">
6        <div className="w-12 h-12 bg-gray-300 rounded-full" />
7        <div className="flex-1 space-y-2">
8          <div className="h-4 bg-gray-300 rounded w-32" />
9          <div className="h-3 bg-gray-200 rounded w-24" />
10        </div>
11      </div>
12
13      {/* Content */}
14      <div className="space-y-2 mb-4">
15        <div className="h-4 bg-gray-300 rounded" />
16        <div className="h-4 bg-gray-300 rounded w-5/6" />
17      </div>
18
19      {/* Image */}
20      <div className="h-64 bg-gray-300 rounded-lg mb-4" />
21
22      {/* Actions */}
23      <div className="flex gap-4">
24        <div className="h-8 w-16 bg-gray-300 rounded" />
25        <div className="h-8 w-16 bg-gray-300 rounded" />
26      </div>
27    </div>
28  );
29}

Tailwind: animate-pulse

animate-pulse
creates a "pulsing" animation - perfect for skeletons!

1<div className="animate-pulse bg-gray-300 h-10 rounded" />

Using the skeleton:

1function PirateFeed() {
2  const [posts, setPosts] = useState<Post[]>([]);
3  const [loading, setLoading] = useState(true);
4
5  useEffect(() => {
6    fetchPosts()
7      .then(data => setPosts(data))
8      .finally(() => setLoading(false));
9  }, []);
10
11  if (loading) {
12    return (
13      <div className="space-y-4">
14        <PostSkeleton />
15        <PostSkeleton />
16        <PostSkeleton />
17      </div>
18    );
19  }
20
21  return (
22    <div className="space-y-4">
23      {posts.map(post => <PostCard key={post.id} post={post} />)}
24    </div>
25  );
26}

Error States - Handling Errors 🚨

What if the API returns an error?

1function PirateFeed() {
2  const [posts, setPosts] = useState<Post[]>([]);
3  const [loading, setLoading] = useState(true);
4  const [error, setError] = useState<string | null>(null);
5
6  useEffect(() => {
7    fetchPosts()
8      .then(data => setPosts(data))
9      .catch(err => setError(err.message))
10      .finally(() => setLoading(false));
11  }, []);
12
13  if (loading) {
14    return <div className="space-y-4">
15      {[...Array(3)].map((_, i) => <PostSkeleton key={i} />)}
16    </div>;
17  }
18
19  if (error) {
20    return (
21      <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
22        <p className="font-bold">Error loading posts</p>
23        <p>{error}</p>
24        <button
25          onClick={() => window.location.reload()}
26          className="mt-2 underline"
27        >
28          Try again
29        </button>
30      </div>
31    );
32  }
33
34  return (
35    <div className="space-y-4">
36      {posts.map(post => <PostCard key={post.id} post={post} />)}
37    </div>
38  );
39}

Empty State - No Data 📭

What if the API returns an empty array?

1if (posts.length === 0) {
2  return (
3    <div className="text-center p-12">
4      <div className="text-6xl mb-4">🏴‍☠️</div>
5      <h2 className="text-2xl font-bold mb-2">No posts yet</h2>
6      <p className="text-gray-600 mb-4">
7        Be the first pirate to post something!
8      </p>
9      <button className="bg-blue-600 text-white px-6 py-2 rounded">
10        Create Post
11      </button>
12    </div>
13  );
14}

Loading Buttons - Button States 🔘

Buttons should also show loading state:

1function SubmitButton() {
2  const [loading, setLoading] = useState(false);
3
4  const handleClick = async () => {
5    setLoading(true);
6    await submitForm();
7    setLoading(false);
8  };
9
10  return (
11    <button
12      onClick={handleClick}
13      disabled={loading}
14      className="bg-blue-600 text-white px-6 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
15    >
16      {loading && (
17        <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
18          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
19          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
20        </svg>
21      )}
22      {loading ? 'Submitting...' : 'Submit'}
23    </button>
24  );
25}

Suspense - React 18 Approach ⚡

React 18 has Suspense for handling loading:

1import { Suspense } from 'react';
2
3function App() {
4  return (
5    <Suspense fallback={<PostSkeleton />}>
6      <PirateFeed />
7    </Suspense>
8  );
9}

Suspense automatically catches loading if you use:

  • React Query / SWR with Suspense mode
  • Next.js Server Components
  • React.lazy() (code splitting)

This is advanced - for now use useState!

Summary 🎓

Loading state -

useState(true)
+
setLoading(false)
Skeleton loaders - placeholder UI mimicking content layout ✅ Error state - catch errors, show message ✅ Empty state - when no data, show friendly UI ✅ Button loading - disabled + spinner ✅ Suspense - React 18 approach (advanced)

UX Rule: Always give feedback to the user - loading, error, success!

See you there! 🚀

Go to CodeWorlds