Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Loading States i Skeletons - UX dla ładowania danych

Użytkownicy nienawidzą czekać bez feedbacku. Loading states i skeleton loaders poprawiają UX pokazując, że coś się dzieje.

Czym są Loading States? ⏳

Loading state to stan aplikacji podczas ładowania danych (np. z API).

Podstawowy przykład:

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..." to nudne. Użytkownicy nie wiedzą ile trzeba czekać.

Skeleton Loaders - Lepszy feedback 💀

Skeleton to placeholder, który naśladuje układ ładowanej treści.

Przykład - Skeleton dla 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
robi "pulsującą" animację - idealną dla skeletonów!

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

Użycie 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 - Obsługa błędów 🚨

Co jeśli API zwróci błąd?

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 - Brak danych 📭

Co jeśli API zwróci pustą tablicę?

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 🔘

Przyciski też powinny pokazywać stan loading:

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 ma Suspense do obsługi loading:

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

Suspense automatycznie wyłapuje loading jeśli używasz:

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

To zaawansowane - na razie używaj useState!

Podsumowanie 🎓

Loading state -

useState(true)
+
setLoading(false)
Skeleton loaders - placeholder UI naśladujący układ treści ✅ Error state - catch errors, pokaż komunikat ✅ Empty state - gdy brak danych, pokaż przyjazny UI ✅ Button loading - disabled + spinner ✅ Suspense - React 18 approach (zaawansowane)

UX Rule: Zawsze dawaj feedback użytkownikowi - loading, error, success!

Do zobaczenia! 🚀

Przejdź do CodeWorlds