Next.js Lesson 4: Layouts & Loading States
Layouts wrap pages and persist across navigation — the navbar and footer render once. Loading states show skeleton UIs while data loads.
Root Layout
// app/layout.tsx — wraps EVERY page
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: { template: "%s | My Site", default: "My Site" },
description: "A Next.js website"
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<Navbar />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
Nested Layouts
// app/dashboard/layout.tsx — only wraps /dashboard/* routes
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<div className="content">{children}</div>
</div>
);
}
// app/dashboard/page.tsx → uses BOTH RootLayout AND DashboardLayout
Loading UI
// app/blog/loading.tsx — shows while blog page loads
export default function BlogLoading() {
return (
<div className="skeleton-container">
{[1,2,3].map(i => (
<div key={i} className="skeleton-card">
<div className="skeleton-title" />
<div className="skeleton-text" />
</div>
))}
</div>
);
}
// app/blog/error.tsx — shows on error
"use client";
export default function BlogError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong: {error.message}</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
🏋️ Practice Task
Create a dashboard section at /dashboard. Add a dashboard layout with a sidebar (links to /dashboard, /dashboard/profile, /dashboard/settings). Create loading.tsx for the dashboard that shows 3 skeleton cards. Create error.tsx with a retry button.
💡 Hint: Sidebar: Overview. DashboardLayout must be at app/dashboard/layout.tsx.