Core Concepts: Pages, Routing, and Components

2. Core Concepts: Pages, Routing, and Components

Now that your Next.js environment is set up, let’s dive into the fundamental concepts that form the backbone of any Next.js application: Pages, the App Router, and React Components (especially Server and Client Components). Understanding these concepts is crucial for building robust and scalable applications.

2.1 Pages and the App Router

In Next.js, a “page” is a React component that is exported from a .js, .jsx, .ts, or .tsx file inside the app directory. The app directory uses file-system routing, which means the structure of your files and folders dictates the URL routes of your application.

The app Directory Structure

When you created your project with create-next-app and chose the App Router, you got a src/app directory. This is where all your routes and UI components will live.

Let’s look at a typical structure:

src/
└── app/
    ├── layout.tsx         # Root Layout
    ├── page.tsx           # Home Page (corresponds to '/')
    ├── dashboard/
    │   ├── layout.tsx     # Dashboard Layout
    │   └── page.tsx       # Dashboard Home Page (corresponds to '/dashboard')
    └── about/
        └── page.tsx       # About Page (corresponds to '/about')
  • page.tsx: This file defines the UI for a route segment. For example, src/app/page.tsx corresponds to the root URL (/), and src/app/about/page.tsx corresponds to /about.
  • layout.tsx: This file defines UI that is shared across multiple pages within a route segment. It wraps its children prop, which represents the page or nested layout beneath it. Layouts are especially powerful for maintaining consistent navigation, headers, footers, or sidebars across a section of your app.

Example: Creating a New Page

Let’s create a new page for your my-nextjs-app.

  1. Create a new folder inside src/app called products.

    mkdir src/app/products
    
  2. Create a page.tsx file inside the products folder.

    touch src/app/products/page.tsx
    
  3. Add the following content to src/app/products/page.tsx:

    // src/app/products/page.tsx
    
    export default function ProductsPage() {
      return (
        <main style={{ padding: '20px', fontFamily: 'sans-serif' }}>
          <h1>Our Products</h1>
          <p>This is where you'll find all our amazing products!</p>
          <ul>
            <li>Laptop Pro</li>
            <li>Smartphone X</li>
            <li>Smartwatch 3</li>
          </ul>
        </main>
      );
    }
    
  4. Save the file. Your development server (npm run dev) should still be running.

  5. Open your browser and navigate to http://localhost:3000/products. You should now see your “Our Products” page!

This demonstrates the simplicity of file-system routing: create a folder, add a page.tsx inside it, and you have a new route.

Nested Routes and Layouts

Layouts allow you to define common UI that wraps pages. A layout component receives a children prop, which will be the pages or nested layouts within its route segment.

  1. Open src/app/layout.tsx. This is your root layout. Notice how it wraps the children (which is your page.tsx for the root route, and any other routes).

    // src/app/layout.tsx
    import type { Metadata } from "next";
    import { Inter } from "next/font/google";
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata: Metadata = {
      title: "Create Next App",
      description: "Generated by create next app",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en">
          <body className={inter.className}>{children}</body>
        </html>
      );
    }
    

    This RootLayout applies to all pages in your application.

  2. Let’s create a nested layout for our /products route. This will allow us to add UI specific to the product section, like a sub-navigation or a different header.

  3. Create a layout.tsx file inside src/app/products.

    touch src/app/products/layout.tsx
    
  4. Add the following content to src/app/products/layout.tsx:

    // src/app/products/layout.tsx
    import Link from 'next/link';
    
    export default function ProductsLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <div style={{ border: '2px solid #0070f3', padding: '15px', borderRadius: '8px' }}>
          <h2 style={{ color: '#0070f3' }}>Product Catalog</h2>
          <nav>
            <ul style={{ listStyle: 'none', padding: 0, display: 'flex', gap: '15px' }}>
              <li><Link href="/products" style={{ textDecoration: 'none', color: '#333' }}>All Products</Link></li>
              <li><Link href="/products/electronics" style={{ textDecoration: 'none', color: '#333' }}>Electronics</Link></li>
              <li><Link href="/products/clothing" style={{ textDecoration: 'none', color: '#333' }}>Clothing</Link></li>
            </ul>
          </nav>
          <div style={{ marginTop: '20px' }}>
            {children} {/* This will render src/app/products/page.tsx or any nested route */}
          </div>
        </div>
      );
    }
    

    Explanation:

    • This ProductsLayout now wraps the ProductsPage.
    • It includes a <h2>Product Catalog</h2> and a navigation bar that will appear on /products and any future nested routes within /products.
  5. Save the file. Go to http://localhost:3000/products in your browser. You should see your new product catalog layout surrounding your product list!

Dynamic Routes

What if you want to display details for a specific product, like /products/laptop-pro? This requires dynamic routes. You can create dynamic routes by enclosing a folder name in square brackets, e.g., [slug] or [id].

  1. Create a dynamic route folder for product details: src/app/products/[productId].

    mkdir src/app/products/[productId]
    
  2. Create a page.tsx file inside src/app/products/[productId].

    touch src/app/products/[productId]/page.tsx
    
  3. Add the following content to src/app/products/[productId]/page.tsx:

    // src/app/products/[productId]/page.tsx
    
    interface ProductDetailPageProps {
      params: {
        productId: string;
      };
    }
    
    export default function ProductDetailPage({ params }: ProductDetailPageProps) {
      const { productId } = params;
    
      // In a real app, you would fetch product details using `productId`
      const products: { [key: string]: string } = {
        'laptop-pro': 'Powerful and sleek laptop for professionals.',
        'smartphone-x': 'The latest generation smartphone with an amazing camera.',
        'smartwatch-3': 'Stay connected and track your fitness with our new smartwatch.',
      };
    
      const productName = productId.replace(/-/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
      const productDescription = products[productId] || 'Product details not found.';
    
      return (
        <main style={{ padding: '20px', fontFamily: 'sans-serif' }}>
          <h3>{productName}</h3>
          <p>{productDescription}</p>
          <p>Detailed view for product ID: `{productId}`</p>
          <p>
            <a href="/products" style={{ textDecoration: 'none', color: '#0070f3' }}>
              &larr; Back to Products
            </a>
          </p>
        </main>
      );
    }
    

    Explanation:

    • The ProductDetailPage component receives params as a prop, which contains the dynamic segments from the URL. Here, params.productId will capture the value from [productId].
    • We simulate fetching product details based on the productId.
  4. Save the file.

  5. Test dynamic routes:

    • Go to http://localhost:3000/products/laptop-pro
    • Then try http://localhost:3000/products/smartphone-x
    • And http://localhost:3000/products/a-non-existent-product

You should see the product details dynamically updated based on the URL segment!

Exercises/Mini-Challenges (Pages and Routing):

  1. Create a Nested Dynamic Route:

    • Inside the src/app/products folder, create a new subfolder called categories.
    • Inside categories, create a dynamic route folder [categorySlug].
    • Inside [categorySlug], create a page.tsx that displays the categorySlug from the URL.
    • Test by navigating to /products/categories/electronics and /products/categories/books.
  2. Add a Link to the Homepage:

    • Modify src/app/products/page.tsx to include a link back to the root (/) homepage using next/link. Remember to import Link from next/link.
    • Hint: import Link from 'next/link'; and then <Link href="/">Go to Home</Link>

2.2 React Components: Server Components vs. Client Components

One of the most significant architectural shifts in modern Next.js (with the App Router) is the distinction between Server Components and Client Components. This concept is central to building performant applications and managing where code executes.

By Default: Server Components

In the App Router, all components (.js, .jsx, .ts, .tsx) inside the app directory are React Server Components (RSC) by default.

Characteristics of Server Components:

  • Rendered on the Server: They execute only on the server, never sent to the client’s browser.
  • Zero JavaScript to Client: No JavaScript bundle size impact on the client, leading to faster initial loads.
  • Direct Access to Backend: Can directly access databases, file systems, and environment variables (server-side only).
  • No State or Effects: Cannot use useState, useEffect, useRef, or other React Hooks that rely on client-side interactivity.
  • No Browser APIs: Cannot use browser-specific APIs like window or localStorage.
  • Can Render Client Components: Server Components can import and render Client Components.

When to use Server Components:

  • Fetching data (e.g., from a database or internal API).
  • Handling sensitive information that shouldn’t reach the client.
  • Rendering static or largely static UI elements.
  • Reducing client-side JavaScript bundle size.

Opting into Client Components: The 'use client' Directive

If a component needs client-side interactivity (state, effects, event handlers, browser APIs), it must be explicitly marked as a Client Component. You do this by adding 'use client'; at the very top of the file, before any imports.

Characteristics of Client Components:

  • Rendered on the Client (Hydration): They are sent to the browser, where React “hydrates” them, making them interactive.
  • Can use State and Effects: Can use useState, useEffect, and other Hooks.
  • Can use Browser APIs: Can access window, localStorage, document, etc.
  • Cannot Directly Access Backend: Cannot directly fetch from databases or read server-side environment variables. Data fetching typically happens via API routes or Server Actions.
  • Can Import Server Components (with caution): While technically possible, it’s generally discouraged to import Server Components directly into Client Components to avoid complex hydration issues. Instead, pass Server Components as children props to Client Components.

When to use Client Components:

  • Adding interactivity (click handlers, input changes, etc.).
  • Managing local component state.
  • Using browser-specific APIs (geolocation, canvas, etc.).
  • Utilizing third-party libraries that rely on React Hooks or browser APIs.

Example: Differentiating Server and Client Components

Let’s modify our products page to include a client-side counter.

  1. Create a new file for our client component: src/app/products/components/ProductCounter.tsx.

    mkdir src/app/products/components
    touch src/app/products/components/ProductCounter.tsx
    
  2. Add the following content to src/app/products/components/ProductCounter.tsx:

    // src/app/products/components/ProductCounter.tsx
    'use client'; // This directive makes it a Client Component
    
    import { useState } from 'react';
    
    interface ProductCounterProps {
      initialCount?: number;
      productName: string;
    }
    
    export default function ProductCounter({ initialCount = 0, productName }: ProductCounterProps) {
      const [count, setCount] = useState(initialCount);
    
      return (
        <div style={{
          border: '1px dashed #ccc',
          padding: '10px',
          margin: '10px 0',
          backgroundColor: '#f9f9f9'
        }}>
          <h4>{productName} Quantity: {count}</h4>
          <button
            onClick={() => setCount(prev => prev + 1)}
            style={{ marginRight: '10px', padding: '8px 12px', cursor: 'pointer' }}
          >
            Add
          </button>
          <button
            onClick={() => setCount(prev => Math.max(0, prev - 1))}
            style={{ padding: '8px 12px', cursor: 'pointer' }}
          >
            Remove
          </button>
        </div>
      );
    }
    

    Notice the 'use client'; directive at the top! This is what makes it a Client Component, allowing it to use useState.

  3. Now, let’s use this Client Component in our Server Component (the products page). Modify src/app/products/page.tsx:

    // src/app/products/page.tsx
    import ProductCounter from './components/ProductCounter'; // Import the client component
    import Link from 'next/link';
    
    export default function ProductsPage() {
      // This is a Server Component. It can directly render other Server or Client Components.
      // We are passing props from the server component to the client component.
    
      const products = [
        { id: 'laptop-pro', name: 'Laptop Pro' },
        { id: 'smartphone-x', name: 'Smartphone X' },
        { id: 'smartwatch-3', name: 'Smartwatch 3' },
      ];
    
      return (
        <main style={{ padding: '20px', fontFamily: 'sans-serif' }}>
          <h1>Our Products</h1>
          <p>This is where you'll find all our amazing products!</p>
          <ul>
            {products.map(product => (
              <li key={product.id}>
                <Link href={`/products/${product.id}`} style={{ textDecoration: 'none', color: '#0070f3' }}>
                  {product.name}
                </Link>
                {/* Render the Client Component */}
                <ProductCounter productName={product.name} initialCount={0} />
              </li>
            ))}
          </ul>
          <p>
            <Link href="/" style={{ textDecoration: 'none', color: '#0070f3' }}>
              &larr; Back to Home
            </Link>
          </p>
        </main>
      );
    }
    
  4. Save both files.

  5. Go to http://localhost:3000/products. You should now see “Add” and “Remove” buttons next to each product. Try clicking them! The counter updates client-side, demonstrating the interactivity of the ProductCounter Client Component within the ProductsPage Server Component.

This fundamental distinction between Server and Client Components is a powerful feature of Next.js that allows you to optimize your application’s performance by minimizing client-side JavaScript where interactivity isn’t needed.

Exercises/Mini-Challenges (Server/Client Components):

  1. Move Link to a Client Component:

    • Create a new client component (e.g., src/app/components/NavLink.tsx) that takes href and children as props.
    • It should use import Link from 'next/link'; and the Link component.
    • Replace the Link components in src/app/products/page.tsx with your new NavLink client component. Ensure it still works correctly.
    • Why might you not want to do this for a simple Link? (Hint: Think about the 'use client' directive and its implications for bundle size.)
  2. Attempt Server-Side Hook Usage:

    • In src/app/products/page.tsx (which is a Server Component), try adding import { useState } from 'react'; and then const [data, setData] = useState(''); inside the ProductsPage function.
    • What error do you get? This error clearly shows that Hooks like useState are forbidden in Server Components because they require client-side interactivity.

By actively experimenting with these concepts, you’ll gain a deeper understanding of how Next.js leverages React’s architecture for optimal performance.