Understanding Next.js Layouts and Pages: A Complete Guide


Introduction
Next.js has revolutionized how we build React applications with its powerful routing system. With the introduction of the App Router in Next.js 13+, the way we structure layouts and pages has been completely reimagined, offering developers more flexibility and control than ever before.
If you've been working with the traditional Pages Router, or if you're completely new to Next.js, understanding the new layout and page system is crucial for building modern, performant web applications.
In this comprehensive guide, we'll explore how to create efficient layouts and pages in Next.js that improve both developer experience and site performance. You'll learn how to structure your application, share UI between routes, and implement advanced patterns that make your code more maintainable.
Understanding File-System Based Routing
At the core of Next.js is file-system based routing, where the folder structure in your project directly maps to the URL structure of your application.
In the App Router, this concept is extended with special files that serve specific purposes:
page.js
- Creates a publicly accessible routelayout.js
- Creates shared UI for a segment and its childrenloading.js
- Creates loading UIerror.js
- Creates error UInot-found.js
- Creates UI for 404 errors
Let's focus on the two most fundamental ones: pages and layouts.
Creating Your First Page
A page in Next.js is UI that is unique to a route. To create a page, add a page.js
file inside the app
directory and export a React component.
Here's how to create a simple home page:
// app/page.tsx
export default function HomePage() {
return (
<div className="container mx-auto py-10">
<h1 className="text-3xl font-bold">Welcome to My Next.js Site</h1>
<p className="mt-4">This is my first Next.js page using the App Router.</p>
</div>
);
}
By default, components inside the app
directory are React Server Components, which means they run on the server. This gives you several advantages:
Direct access to backend resources (databases, file systems)
Keeping sensitive information on the server
Automatic code-splitting
No client-side JavaScript by default (unless needed)
Understanding Layouts
A layout is shared UI that wraps multiple pages. The key benefit of layouts is that they preserve state, remain interactive, and don't re-render when a user navigates between pages that share the layout.
Root Layout
Every Next.js application requires a root layout, which is defined in app/layout.js
. This layout wraps all pages in your application and must include html
and body
tags:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header className="bg-slate-800 text-white p-4">
<nav className="container mx-auto">
<h1 className="text-xl font-bold">My Next.js Blog</h1>
</nav>
</header>
//or you can create a component in components folder and you can import here.
<main className="container mx-auto py-8">{children}</main>
<footer className="bg-slate-800 text-white p-4">
<div className="container mx-auto">
<p>© 2025 My Next.js Blog</p>
</div>
</footer>
</body>
</html>
);
}
The children
prop represents the page content or nested layout that will be rendered inside this layout.
Creating Nested Routes and Layouts
One of the most powerful features of the App Router is the ability to create nested layouts that apply to specific route segments.
Folder Structure for Nested Routes
Let's say we want to create a blog section with the following routes:
/blog
- Blog index page/blog/[slug]
- Individual blog post pages
Here's how we would structure our folders:
app/
├── layout.tsx # Root layout (applies to all routes)
├── page.tsx # Home page (/)
└── blog/
├── layout.tsx # Blog layout (applies to /blog and /blog/[slug])
├── page.tsx # Blog index page (/blog)
└── [slug]/
└── page.tsx # Individual blog post page (/blog/post-1, /blog/post-2, etc.)
Creating a Nested Layout
Now, let's create a layout specifically for our blog section:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
{/* Main content area */}
{children}
</div>
<div className="md:col-span-1">
{/* Blog sidebar */}
<div className="bg-slate-100 p-4 rounded-lg">
<h2 className="text-xl font-bold mb-4">Recent Posts</h2>
<ul className="space-y-2">
<li><a href="/blog/getting-started" className="text-blue-600 hover:underline">Getting Started with Next.js</a></li>
<li><a href="/blog/server-components" className="text-blue-600 hover:underline">Understanding Server Components</a></li>
<li><a href="/blog/data-fetching" className="text-blue-600 hover:underline">Data Fetching Strategies</a></li>
</ul>
</div>
</div>
</div>
);
}
This layout will be applied to the blog index page and all individual blog post pages, giving them a consistent sidebar while allowing the main content to change.
Creating the Blog Index Page
// app/blog/page.tsx
export default function BlogIndexPage() {
return (
<div>
<h1 className="text-3xl font-bold mb-6">Blog</h1>
<div className="space-y-6">
{/* Blog post previews */}
<article className="border-b pb-6">
<h2 className="text-2xl font-bold mb-2">
<a href="/blog/getting-started" className="text-blue-600 hover:underline">
Getting Started with Next.js
</a>
</h2>
<p className="text-slate-600 mb-2">Published on March 15, 2025</p>
<p>Learn how to set up your first Next.js project and understand the basics of the framework.</p>
</article>
<article className="border-b pb-6">
<h2 className="text-2xl font-bold mb-2">
<a href="/blog/server-components" className="text-blue-600 hover:underline">
Understanding Server Components
</a>
</h2>
<p className="text-slate-600 mb-2">Published on March 10, 2025</p>
<p>Dive deep into React Server Components and how they change the way we build applications.</p>
</article>
<article className="border-b pb-6">
<h2 className="text-2xl font-bold mb-2">
<a href="/blog/data-fetching" className="text-blue-600 hover:underline">
Data Fetching Strategies
</a>
</h2>
<p className="text-slate-600 mb-2">Published on March 5, 2025</p>
<p>Explore different ways to fetch data in Next.js and choose the right approach for your needs.</p>
</article>
</div>
</div>
);
}
Creating a Dynamic Blog Post Page
For individual blog posts, we'll use a dynamic route segment with [slug]
:
// app/blog/[slug]/page.tsx
export default function BlogPostPage({
params,
}: {
params: { slug: string }
}) {
// In a real application, you would fetch the blog post data based on the slug
return (
<article>
<h1 className="text-3xl font-bold mb-2">Blog Post: {params.slug}</h1>
<p className="text-slate-600 mb-6">Published on March 15, 2025</p>
<div className="prose max-w-none">
<p>This is a blog post about {params.slug.replace(/-/g, ' ')}. In a real application, this content would be fetched from a CMS or database.</p>
<h2>Introduction</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod, nisl eget aliquam ultricies, nunc nisl aliquet nunc, quis aliquam nisl nunc quis nisl.</p>
<h2>Main Content</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante.</p>
<h2>Conclusion</h2>
<p>Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
</div>
</article>
);
}
Client and Server Components
Next.js 13+ introduced a fundamental distinction between Server Components and Client Components. Understanding when to use each is crucial for building efficient applications.
Server Components (Default)
By default, components in the app
directory are Server Components. They:
Run on the server
Can access server resources directly
Don't include client-side JavaScript
Can't use hooks like
useState
or event handlers
Client Components
To make a component run on the client, add the "use client"
directive at the top of the file:
"use client"
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Interleaving Client and Server Components
A powerful pattern in Next.js is passing Server Components as children to Client Components. This allows you to:
Keep heavy data fetching on the server
Only send interactive parts to the client
For example:
// app/blog/page.tsx (Server Component)
import BlogSidebar from './blog-sidebar';
import ClientPostList from './client-post-list';
import { getPosts } from '@/lib/posts';
export default async function BlogPage() {
const posts = await getPosts(); // Server-side data fetching
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
<ClientPostList>
{/* Pass server-fetched data to a client component */}
{posts.map(post => (
<BlogPostPreview key={post.id} post={post} />
))}
</ClientPostList>
</div>
<BlogSidebar />
</div>
);
}
// BlogPostPreview.tsx (Server Component)
export function BlogPostPreview({ post }) {
return (
<article className="border-b pb-6">
<h2 className="text-2xl font-bold mb-2">
<a href={`/blog/${post.slug}`} className="text-blue-600 hover:underline">
{post.title}
</a>
</h2>
<p className="text-slate-600 mb-2">Published on {post.date}</p>
<p>{post.excerpt}</p>
</article>
);
}
// client-post-list.tsx (Client Component)
"use client"
import { useState } from 'react';
export default function ClientPostList({ children }) {
const [sortOrder, setSortOrder] = useState('newest');
return (
<div>
<div className="mb-4">
<label className="mr-2">Sort by:</label>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
className="border p-1 rounded"
>
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
</select>
</div>
<div className="space-y-6">
{children}
</div>
</div>
);
}
Data Fetching in Layouts and Pages
One of the major advantages of the App Router is the ability to fetch data at any level of your component tree.
Fetching Data in Layouts
You can fetch data directly in layouts, which is useful for data that's shared across multiple pages:
// app/blog/layout.tsx
import { getCategories } from '@/lib/categories';
export default async function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
const categories = await getCategories();
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
{children}
</div>
<div className="md:col-span-1">
<div className="bg-slate-100 p-4 rounded-lg">
<h2 className="text-xl font-bold mb-4">Categories</h2>
<ul className="space-y-2">
{categories.map(category => (
<li key={category.id}>
<a href={`/blog/category/${category.slug}`} className="text-blue-600 hover:underline">
{category.name}
</a>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
Parallel Data Fetching
Next.js allows you to fetch multiple pieces of data in parallel, which can significantly improve loading performance:
// app/dashboard/page.tsx
import { getUser } from '@/lib/user';
import { getRecentPosts } from '@/lib/posts';
import { getAnalytics } from '@/lib/analytics';
export default async function DashboardPage() {
// These requests run in parallel
const [user, recentPosts, analytics] = await Promise.all([
getUser(),
getRecentPosts(),
getAnalytics()
]);
return (
<div>
<h1>Dashboard</h1>
<div className="user-info">
<h2>Welcome, {user.name}</h2>
</div>
<div className="recent-posts">
<h2>Recent Posts</h2>
{/* Display recent posts */}
</div>
<div className="analytics">
<h2>Analytics</h2>
{/* Display analytics */}
</div>
</div>
);
}
Navigation Between Pages
Next.js provides the <Link>
component for client-side navigation between pages.
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<ul className="flex space-x-4">
<li>
<Link
href="/"
className="text-blue-600 hover:underline"
>
Home
</Link>
</li>
<li>
<Link
href="/blog"
className="text-blue-600 hover:underline"
>
Blog
</Link>
</li>
<li>
<Link
href="/about"
className="text-blue-600 hover:underline"
>
About
</Link>
</li>
<li>
<Link
href="/contact"
className="text-blue-600 hover:underline"
>
Contact
</Link>
</li>
</ul>
</nav>
);
}
The <Link>
component automatically prefetches pages that are in the viewport, making navigation feel instant.
Advanced Patterns
Loading UI
Next.js allows you to create loading states for your routes using a special loading.js
file:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<div className="h-8 bg-gray-200 rounded animate-pulse w-1/3"></div>
<div className="space-y-6">
{[1, 2, 3].map(i => (
<div key={i} className="space-y-2">
<div className="h-6 bg-gray-200 rounded animate-pulse w-3/4"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-1/4"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-full"></div>
<div className="h-4 bg-gray-200 rounded animate-pulse w-2/3"></div>
</div>
))}
</div>
</div>
);
}
Error Handling
You can create custom error UI using the error.js
file:
// app/blog/error.tsx
"use client"
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div className="p-8 text-center">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<p className="mb-4">We're sorry, but we couldn't load the blog posts.</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try again
</button>
</div>
);
}
Route Groups
Route groups allow you to organize your routes without affecting the URL structure:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx
│ ├── contact/
│ │ └── page.tsx
│ └── layout.tsx
└── (dashboard)/
├── analytics/
│ └── page.tsx
├── settings/
│ └── page.tsx
└── layout.tsx
In this example, the parentheses ()
create route groups that don't affect the URL path. This allows you to have different layouts for different sections of your site.
Best Practices and Performance Tips
1. Keep Layouts Light
Layouts should be as lightweight as possible since they wrap multiple pages. Heavy layouts can impact the performance of your entire application.
2. Use Server Components by Default
Start with Server Components and only switch to Client Components when you need interactivity or client-side state.
3. Colocate Related Files
Keep related files close to each other in the file system. For example, place component-specific styles and utilities in the same directory as the component.
4. Optimize Images
Always use the Next.js <Image>
component for optimized image loading:
import Image from 'next/image';
export default function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="User avatar"
width={40}
height={40}
className="rounded-full"
/>
);
}
5. Implement Streaming for Large Pages
For pages with a lot of data fetching, use React Suspense to stream parts of the page as they become ready:
import { Suspense } from 'react';
import Loading from './loading';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Loading />}>
{/* This component can fetch its own data */}
<UserProfile />
</Suspense>
<Suspense fallback={<Loading />}>
{/* This component can fetch its own data */}
<RecentActivity />
</Suspense>
</div>
);
}
Conclusion
Next.js layouts and pages in the App Router provide a powerful and flexible way to structure your application. By understanding how to create and nest layouts, work with Server and Client Components, and implement advanced patterns, you can build highly performant and maintainable web applications.
The key takeaways from this guide are:
Use the file system to define your routes and UI hierarchy
Leverage layouts to share UI between pages while preserving state
Choose Server Components by default and only use Client Components when necessary
Fetch data at the component level where it's needed
Use the built-in features for loading states, error handling, and navigation
As you continue your Next.js journey, experiment with these patterns and find the approach that works best for your specific use case. The flexibility of the App Router allows for many different architectural decisions, so don't be afraid to adapt these guidelines to your needs.
Happy coding!
Subscribe to my newsletter
Read articles from Arab Amer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Arab Amer
Arab Amer
I'm a passionate Frontend and Java Developer with a strong focus on building modern, scalable web applications. Currently, I work at a startup, where I contribute to creating dynamic user experiences using Next.js and React.js. I love sharing my knowledge through blogs, helping developers learn and grow in the ever-evolving world of frontend development. Constantly exploring new technologies, I aim to blend performance, design, and functionality in every project I work on.