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.tsxcorresponds to the root URL (/), andsrc/app/about/page.tsxcorresponds to/about.layout.tsx: This file defines UI that is shared across multiple pages within a route segment. It wraps itschildrenprop, 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.
Create a new folder inside
src/appcalledproducts.mkdir src/app/productsCreate a
page.tsxfile inside theproductsfolder.touch src/app/products/page.tsxAdd 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> ); }Save the file. Your development server (
npm run dev) should still be running.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.
Open
src/app/layout.tsx. This is your root layout. Notice how it wraps thechildren(which is yourpage.tsxfor 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
RootLayoutapplies to all pages in your application.Let’s create a nested layout for our
/productsroute. This will allow us to add UI specific to the product section, like a sub-navigation or a different header.Create a
layout.tsxfile insidesrc/app/products.touch src/app/products/layout.tsxAdd 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
ProductsLayoutnow wraps theProductsPage. - It includes a
<h2>Product Catalog</h2>and a navigation bar that will appear on/productsand any future nested routes within/products.
- This
Save the file. Go to
http://localhost:3000/productsin 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].
Create a dynamic route folder for product details:
src/app/products/[productId].mkdir src/app/products/[productId]Create a
page.tsxfile insidesrc/app/products/[productId].touch src/app/products/[productId]/page.tsxAdd 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' }}> ← Back to Products </a> </p> </main> ); }Explanation:
- The
ProductDetailPagecomponent receivesparamsas a prop, which contains the dynamic segments from the URL. Here,params.productIdwill capture the value from[productId]. - We simulate fetching product details based on the
productId.
- The
Save the file.
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
- Go to
You should see the product details dynamically updated based on the URL segment!
Exercises/Mini-Challenges (Pages and Routing):
Create a Nested Dynamic Route:
- Inside the
src/app/productsfolder, create a new subfolder calledcategories. - Inside
categories, create a dynamic route folder[categorySlug]. - Inside
[categorySlug], create apage.tsxthat displays thecategorySlugfrom the URL. - Test by navigating to
/products/categories/electronicsand/products/categories/books.
- Inside the
Add a Link to the Homepage:
- Modify
src/app/products/page.tsxto include a link back to the root (/) homepage usingnext/link. Remember to importLinkfromnext/link. - Hint:
import Link from 'next/link';and then<Link href="/">Go to Home</Link>
- Modify
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
windoworlocalStorage. - 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
childrenprops 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.
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.tsxAdd 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 useuseState.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' }}> ← Back to Home </Link> </p> </main> ); }Save both files.
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 theProductCounterClient Component within theProductsPageServer 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):
Move
Linkto a Client Component:- Create a new client component (e.g.,
src/app/components/NavLink.tsx) that takeshrefandchildrenas props. - It should use
import Link from 'next/link';and theLinkcomponent. - Replace the
Linkcomponents insrc/app/products/page.tsxwith your newNavLinkclient 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.)
- Create a new client component (e.g.,
Attempt Server-Side Hook Usage:
- In
src/app/products/page.tsx(which is a Server Component), try addingimport { useState } from 'react';and thenconst [data, setData] = useState('');inside theProductsPagefunction. - What error do you get? This error clearly shows that Hooks like
useStateare forbidden in Server Components because they require client-side interactivity.
- In
By actively experimenting with these concepts, you’ll gain a deeper understanding of how Next.js leverages React’s architecture for optimal performance.