Menu

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>&copy; 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)

Anonymous November 22, 2025

Good

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?