Implementing Skeleton Loading in Next.js for a Table

Step 1: Project Structure (in an Existing App)

Assume your app is already set up with the app/ directory. Add these files for a table page at /users:

app/
  users/
    page.tsx       // Your main table page (fetches and renders data)
    loading.tsx    // Separate skeleton file (called automatically by Next.js)

Step 2: Skeleton Component (Separate Loading Page)

Create app/users/loading.tsx. This exports a default component that renders a skeleton mimicking your table structure. Next.js automatically wraps your page.tsx in Suspense and shows this during loading.

// app/users/loading.tsx
import { Skeleton } from "@/components/ui/skeleton"; // Optional: Use shadcn/ui Skeleton if installed


export default function Loading() {
  return (
    <div className="container mx-auto p-4">
      <div className="flex justify-between items-center mb-6">
        <Skeleton className="h-8 w-48" /> {/* Header title skeleton */}
        <Skeleton className="h-10 w-32" /> {/* Button skeleton */}
      </div>
      <div className="rounded-md border">
        <table className="w-full">
          <thead>
            <tr className="border-b">
              <th className="text-left p-4 font-medium">ID</th>
              <th className="text-left p-4 font-medium">Name</th>
              <th className="text-left p-4 font-medium">Email</th>
              <th className="text-left p-4 font-medium">Actions</th>
            </tr>
          </thead>
          <tbody>
            {[...Array(5)].map((_, i) => ( // 5 placeholder rows
              <tr key={i} className="border-b hover:bg-muted/50">
                <td className="p-4"><Skeleton className="h-4 w-12" /></td>
                <td className="p-4"><Skeleton className="h-4 w-64" /></td>
                <td className="p-4"><Skeleton className="h-4 w-80" /></td>
                <td className="p-4"><Skeleton className="h-8 w-20" /></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="flex justify-end mt-4">
        <Skeleton className="h-10 w-24" /> {/* Pagination skeleton */}
      </div>
    </div>
  );
}

  • Why separate? Next.js detects loading.tsx and uses it as the Suspense fallback only for this route (/users). No manual wrapping needed.
  • Customization: Adjust row count, widths, and heights to match your real table. Add shimmer animation with Tailwind: animate-pulse on <tr> or <td>.
  • If no shadcn/ui: Replace Skeleton with a div like <div className=”h-4 w-full bg-muted rounded animate-pulse” />.

Step 3: Main Table Page (Integrate into Existing App)

Update app/users/page.tsx. This fetches data (simulate with fetch for async loading). The skeleton shows automatically while data loads.

// app/users/page.tsx
import { Suspense } from "react"; // Already handled by Next.js, but explicit for clarity


interface User {
  id: number;
  name: string;
  email: string;
}


async function fetchUsers(): Promise<User[]> {
  // Simulate API delay; replace with your real endpoint
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  if (!res.ok) throw new Error("Failed to fetch users");
  return res.json();
}


export default async function UsersPage() {
  const users = await fetchUsers(); // This triggers the loading state


  return (
    <div className="container mx-auto p-4">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Users</h1>
        <button className="px-4 py-2 bg-blue-500 text-white rounded">Add User</button>
      </div>
      <div className="rounded-md border">
        <table className="w-full">
          <thead>
            <tr className="border-b bg-muted/50">
              <th className="text-left p-4 font-medium">ID</th>
              <th className="text-left p-4 font-medium">Name</th>
              <th className="text-left p-4 font-medium">Email</th>
              <th className="text-left p-4 font-medium">Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map((user) => (
              <tr key={user.id} className="border-b hover:bg-muted/50">
                <td className="p-4">{user.id}</td>
                <td className="p-4">{user.name}</td>
                <td className="p-4">{user.email}</td>
                <td className="p-4">
                  <button className="px-2 py-1 bg-green-500 text-white rounded text-sm">Edit</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="flex justify-end mt-4">
        <button className="px-4 py-2 bg-gray-500 text-white rounded">Next Page</button>
      </div>
    </div>
  );
}

How it works: When navigating to /users, Next.js shows loading.tsx immediately (server-rendered for speed). Once fetchUsers() resolves, it streams in the real table, swapping seamlessly.

In existing app: Drop this into any route folder (e.g., app/dashboard/). If your table is client-side (e.g., uses useState/useEffect), wrap the table in <Suspense fallback={<YourSkeleton />}> instead.

Error handling: Add app/users/error.tsx for failures (similar convention).

Leave a comment

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