0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · درس

إنشاء loading.tsx وعناصر Skeleton ذات معنى

أنشئ حالات تحميل على مستوى المسار وعناصر نائبة تطابق التخطيط النهائي لتجنب التحرك البصري.

إنشاء loading.tsx وعناصر Skeleton ذات معنى درس مجاني في Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Next.js 15 Fullstack (App Router + Server Actions)، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why loading.tsx Exists

In the App Router, a loading.tsx file is special: Next.js automatically wraps your route segment's page.tsx in a React <Suspense> boundary and shows loading.tsx as the fallback while the server component streams.

  • You get an instant loading state with zero manual Suspense wiring.
  • It only triggers on the server-render of that segment, not on every client interaction.
  • The fallback is shown immediately while the page's async data resolves.

This lesson is about making that fallback meaningful so the user perceives speed and the layout does not jump.

The File Convention

Drop a loading.tsx next to your page.tsx in any route segment. Its default export is rendered as the Suspense fallback for that segment and everything below it.

  • It is a normal React component — it can be a Server Component (the default) since it renders only static markup.
  • No props are passed to it; it must be self-contained.
  • Keep it lightweight so it ships and paints fast.
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="dashboard-grid" aria-busy="true">
      <DashboardSkeleton />
    </div>
  );
}

The Real Goal: Avoid Layout Shift

A bad loading state replaces your page with a centered spinner, then snaps to a totally different layout when data arrives. That visual jump hurts perceived performance and Cumulative Layout Shift (CLS).

A good skeleton mirrors the final layout:

  • Same number of cards, rows, and columns.
  • Same approximate widths and heights as the real content.
  • Same spacing and grid structure.

When real data swaps in, nothing moves — only the gray placeholders fill with content.

A Reusable Skeleton Primitive

Start with one tiny building block: a Skeleton box that renders a gray, rounded rectangle. You compose everything else from it.

  • Accept className so callers control width, height, and shape.
  • Add an animate-pulse style (Tailwind) or a CSS shimmer for the loading feel.
  • Mark it aria-hidden — it is decorative, not real content.
// components/skeleton.tsx
export function Skeleton({ className = '' }: { className?: string }) {
  return (
    <div
      aria-hidden="true"
      className={`animate-pulse rounded-md bg-gray-200 ${className}`}
    />
  );
}

Matching the Final Layout

Build the skeleton by copying the structure of the real component, then replacing text and images with Skeleton boxes sized to match.

  • An avatar becomes a circle: h-10 w-10 rounded-full.
  • A title line becomes a wide bar; a subtitle becomes a shorter, thinner bar.
  • Use the same container classes (padding, gap, border) as the real card.
// components/user-card-skeleton.tsx
import { Skeleton } from './skeleton';

export function UserCardSkeleton() {
  return (
    <div className="flex items-center gap-4 rounded-lg border p-4">
      <Skeleton className="h-10 w-10 rounded-full" />
      <div className="flex-1 space-y-2">
        <Skeleton className="h-4 w-1/3" />
        <Skeleton className="h-3 w-1/2" />
      </div>
    </div>
  );
}

Lists: Repeat the Row Skeleton

For lists and tables, render a fixed count of row skeletons — enough to fill the typical viewport so the page looks complete.

  • Reuse the single-row skeleton inside an array.
  • Pick a count that roughly matches your usual page size (e.g. 6–8 rows).
  • Keep the same gap and wrapper as the real list to preserve spacing.
// components/user-list-skeleton.tsx
import { UserCardSkeleton } from './user-card-skeleton';

export function UserListSkeleton({ rows = 6 }: { rows?: number }) {
  return (
    <div className="space-y-3">
      {Array.from({ length: rows }).map((_, i) => (
        <UserCardSkeleton key={i} />
      ))}
    </div>
  );
}

Wiring the Skeleton into loading.tsx

Now loading.tsx just composes your skeleton components inside the same outer layout as the page. The header that is static (not data-dependent) can be rendered for real even in the loading state.

  • Render the real, instant parts (page title, tabs).
  • Swap only the data-bound regions for skeletons.
  • This gives a page that feels half-loaded already.
// app/dashboard/users/loading.tsx
import { UserListSkeleton } from '@/components/user-list-skeleton';

export default function Loading() {
  return (
    <section className="mx-auto max-w-2xl p-6">
      <h1 className="mb-4 text-2xl font-bold">Users</h1>
      <UserListSkeleton rows={6} />
    </section>
  );
}

loading.tsx vs Component-Level Suspense

loading.tsx covers the whole segment — if any data on the page is slow, the entire fallback shows. Sometimes you want finer control so fast content paints first.

  • Use loading.tsx for the segment-wide first paint.
  • Wrap individual slow components in <Suspense> with their own skeleton fallback to stream them independently.
  • Combine both: a light segment skeleton, plus granular Suspense for the slowest widget.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { RevenueChart } from './revenue-chart';
import { ChartSkeleton } from './chart-skeleton';

export default function Page() {
  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold">Overview</h1>
      {/* Static + fast content paints immediately */}
      <Suspense fallback={<ChartSkeleton />}>
        {/* Slow async server component streams in later */}
        <RevenueChart />
      </Suspense>
    </main>
  );
}

Pure Function for Skeleton Counts

How many skeleton rows should you show? A small helper keeps it consistent and clamps to a sane range so you never render an absurd number of placeholders.

This logic is plain TypeScript — no framework involved — so you can unit-test it in isolation.

function skeletonRowCount(pageSize: number, viewportRows = 8): number {
  if (!Number.isFinite(pageSize) || pageSize <= 0) return viewportRows;
  return Math.min(pageSize, viewportRows);
}

console.log(skeletonRowCount(20));  // 8  (clamped to viewport)
console.log(skeletonRowCount(3));   // 3  (fewer items than viewport)
console.log(skeletonRowCount(0));   // 8  (fallback default)
console.log(skeletonRowCount(-5));  // 8  (guards invalid input)

Accessibility & Reduced Motion

Skeletons are decorative, but they still affect assistive tech and motion-sensitive users.

  • Mark the loading region with aria-busy="true" and individual placeholders with aria-hidden="true".
  • Provide a visually-hidden status like <span className="sr-only">Loading users</span> for screen readers.
  • Respect prefers-reduced-motion so the pulse animation does not run for users who opt out.
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .animate-pulse {
    animation: none;
  }
}

Common Pitfalls

Avoid these mistakes that defeat the purpose of a skeleton:

  • Spinner-only fallback: gives no layout cue and guarantees a shift when content arrives.
  • Mismatched sizes: if the skeleton card is 60px tall but the real card is 96px, the page still jumps.
  • Forgetting the wrapper: different padding/grid between skeleton and page shifts everything.
  • Skeleton over fast data: if a segment loads in <100ms, the flash of skeleton can feel worse — consider component-level Suspense for only the slow parts.

Quick Check

Test your understanding of route-level loading states and skeletons.

Recap

You learned how to craft meaningful loading states in the App Router:

  • loading.tsx is an automatic Suspense fallback for its route segment.
  • Build skeletons from a small Skeleton primitive and compose them to mirror the final layout.
  • Match wrappers, counts, and dimensions to avoid layout shift (CLS).
  • Render instant static parts (titles, tabs) for real; skeleton only the data-bound regions.
  • Use loading.tsx for segment-wide first paint and component-level <Suspense> to stream slow widgets independently.
  • Handle accessibility with aria-busy, aria-hidden, an sr-only status, and prefers-reduced-motion.

الأسئلة الشائعة

هل درس «إنشاء loading.tsx وعناصر Skeleton ذات معنى» مجاني؟

نعم — نص درس «إنشاء loading.tsx وعناصر Skeleton ذات معنى» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Next.js 15 Fullstack (App Router + Server Actions)، انتقل إلى CoddyKit PRO. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.

ماذا ستتعلم في «إنشاء loading.tsx وعناصر Skeleton ذات معنى»؟

أنشئ حالات تحميل على مستوى المسار وعناصر نائبة تطابق التخطيط النهائي لتجنب التحرك البصري. تتمرن على Next.js 15 Fullstack (App Router + Server Actions) مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Next.js 15 Fullstack (App Router + Server Actions)؟

لا تُشترط خبرة سابقة. Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «إنشاء loading.tsx وعناصر Skeleton ذات معنى»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Next.js 15 Fullstack (App Router + Server Actions) هذا؟

نعم. كل درس في Next.js 15 Fullstack (App Router + Server Actions) يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. حدود Suspense والبث على مستوى المكوّن
  2. إنشاء loading.tsx وعناصر Skeleton ذات معنى
  3. العرض المسبق الجزئي: غلاف ثابت وثقوب ديناميكية
  4. مشكلات البث: تحرك التخطيط والشلالات
← العودة إلى Next.js 15 Fullstack (App Router + Server Actions)