React & Next.js Topic
Next.js File-Based Routing & App Router Structure
Next.js Routing Evolution
Next.js has two routing systems:
-
Pages Router (original, stable)
-
App Router (new, recommended for new projects)
We'll focus on the App Router as it's the future of Next.js.
File-Based Routing Fundamentals
Basic Route Structure
text
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── globals.css # Global styles
├── about/
│ ├── page.tsx # /about
│ └── layout.tsx # Layout for about section
├── blog/
│ ├── page.tsx # /blog
│ ├── layout.tsx # Layout for blog section
│ └── [slug]/
│ └── page.tsx # Dynamic routes: /blog/hello-world
└── dashboard/
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
1. Pages (page.tsx)
Every page.tsx file represents a route segment. The component exported from page.tsx is rendered when that route is accessed.
tsx
// app/page.tsx - Home page (/)
export default function HomePage() {
return (
<main>
<h1>Welcome to the Home Page</h1>
</main>
);
}
// app/about/page.tsx - About page (/about)
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
);
}
// app/blog/page.tsx - Blog list page (/blog)
export default function BlogPage() {
return (
<div>
<h1>Blog Posts</h1>
{/* List of blog posts */}
</div>
);
}
2. Layouts (layout.tsx)
Layouts wrap pages and other layouts. They persist across navigation and are used for shared UI.
tsx
// app/layout.tsx - Root layout (applies to all routes)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>{children}</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
);
}
// app/blog/layout.tsx - Blog section layout
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="blog-container">
<aside className="blog-sidebar">
<h2>Blog Categories</h2>
{/* Sidebar content */}
</aside>
<section className="blog-content">
{children} {/* This will be the blog page */}
</section>
</div>
);
}
3. Nested Routing & Parallel Routes
tsx
// Complex nested structure app/ ├── dashboard/ │ ├── layout.tsx # Dashboard main layout │ ├── page.tsx # /dashboard │ ├── analytics/ │ │ └── page.tsx # /dashboard/analytics │ ├── settings/ │ │ └── page.tsx # /dashboard/settings │ └── @team/ // PARALLEL ROUTES (advanced) │ ├── default.tsx │ └── page.tsx
Dynamic Routes
Basic Dynamic Routes
tsx
// app/blog/[slug]/page.tsx
interface BlogPostParams {
params: {
slug: string;
};
}
export default function BlogPost({ params }: BlogPostParams) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
{/* Blog content */}
</article>
);
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((res) => res.json());
return posts.map((post: any) => ({
slug: post.slug,
}));
}
Catch-All Segments
tsx
// app/docs/[...path]/page.tsx
// Matches: /docs, /docs/getting-started, /docs/getting-started/installation, etc.
interface DocsParams {
params: {
path: string[];
};
}
export default function DocsPage({ params }: DocsParams) {
const path = params.path || [];
return (
<div>
<h1>Documentation</h1>
<p>Current path: {path.join(' / ')}</p>
</div>
);
}
Optional Catch-All Segments
tsx
// app/shop/[[...path]]/page.tsx
// Matches: /shop, /shop/category, /shop/category/product
export default function ShopPage({ params }: { params: { path?: string[] } }) {
const path = params.path || [];
if (path.length === 0) {
return <h1>All Products</h1>;
}
return (
<div>
<h1>Shop: {path.join(' → ')}</h1>
</div>
);
}
Server Components vs Client Components
The Fundamental Difference
| Aspect | Server Components | Client Components |
|---|---|---|
| Bundle Size | Zero bundle impact | Increase bundle size |
| Data Fetching | Direct database/API access | useEffect/fetch |
| Interactivity | No event handlers | Full interactivity |
| State | No useState/useReducer | Full state management |
| Effects | No useEffect | Full lifecycle |
| DOM APIs | No access | Full access |
Server Components (Default)
tsx
// app/products/page.tsx - SERVER COMPONENT (default)
import { db } from '@/lib/db';
// This runs on the server - can directly access databases
async function getProducts() {
const products = await db.product.findMany();
return products;
}
export default async function ProductsPage() {
const products = await getProducts(); // Direct database call
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
{/* This is static - no interactivity */}
</li>
))}
</ul>
</div>
);
}
Client Components
tsx
// components/AddToCartButton.tsx - CLIENT COMPONENT
'use client'; // This directive makes it a Client Component
import { useState } from 'react';
export default function AddToCartButton({ productId }: { productId: string }) {
const [quantity, setQuantity] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const handleAddToCart = async () => {
setIsLoading(true);
try {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId, quantity }),
});
// Show success message, update UI, etc.
} finally {
setIsLoading(false);
}
};
return (
<div>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
min="1"
/>
<button onClick={handleAddToCart} disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add to Cart'}
</button>
</div>
);
}
Mixing Server and Client Components
tsx
// app/products/page.tsx - SERVER COMPONENT
import { db } from '@/lib/db';
import AddToCartButton from '@/components/AddToCartButton';
import SearchBar from '@/components/SearchBar';
async function getProducts() {
const products = await db.product.findMany();
return products;
}
export default async function ProductsPage() {
const products = await getProducts(); // Server-side data fetching
return (
<div>
<h1>Products</h1>
{/* Client Component for interactive search */}
<SearchBar />
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
{/* Client Component for interactivity */}
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
</div>
);
}
Advanced App Router Features
Loading UI (loading.tsx)
tsx
// app/blog/loading.tsx - Automatic loading state
export default function BlogLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
</div>
);
}
Error Handling (error.tsx)
tsx
// app/blog/error.tsx - Error boundary
'use client';
export default function BlogError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="error-container">
<h2>Something went wrong loading the blog!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Route Groups
tsx
// Organize routes without affecting URL structure app/ ├── (auth)/ │ ├── login/ │ │ └── page.tsx # /login │ └── register/ │ └── page.tsx # /register ├── (marketing)/ │ ├── about/ │ │ └── page.tsx # /about │ └── contact/ │ └── page.tsx # /contact └── layout.tsx
Private Routes Pattern
tsx
// app/dashboard/layout.tsx - Protected route
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect('/login');
}
return (
<div className="dashboard-layout">
<aside>Dashboard Sidebar</aside>
<main>{children}</main>
</div>
);
}
Best Practices & Patterns
1. Colocation Strategy
text
app/ ├── blog/ │ ├── components/ # Components used only in blog │ │ ├── BlogCard.tsx │ │ └── BlogList.tsx │ ├── actions/ # Server actions for blog │ │ └── create-post.ts │ ├── layout.tsx │ └── page.tsx
2. Data Fetching Patterns
tsx
// Server Component with multiple data fetches
export default async function ProductPage({ params }: { params: { id: string } }) {
// Parallel data fetching
const [product, reviews, relatedProducts] = await Promise.all([
fetchProduct(params.id),
fetchReviews(params.id),
fetchRelatedProducts(params.id),
]);
return (
<div>
<ProductDetails product={product} />
<ProductReviews reviews={reviews} />
<RelatedProducts products={relatedProducts} />
</div>
);
}
3. Client Component Hydration
tsx
// components/ClientSideComponent.tsx
'use client';
import { useEffect, useState } from 'react';
export default function ClientSideComponent({ serverData }: { serverData: any }) {
const [clientData, setClientData] = useState(serverData);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
// Client-side only logic
}, []);
if (!isClient) {
// Render server-provided data during SSR
return <div>{serverData}</div>;
}
// Client-side interactive rendering
return <div>{clientData}</div>;
}
When to Use Each Component Type
Use Server Components for:
-
Data fetching
-
Direct database access
-
Keeping sensitive information on server
-
Large dependencies
-
Static content
Use Client Components for:
-
Interactivity and event listeners
-
State and lifecycle effects
-
Browser-only APIs
-
Custom hooks that use state/effects
-
UI that needs to update frequently
The App Router's mental model: "Start with Server Components, drop down to Client Components when you need interactivity."
Comments (1)
Good