Next.js is a powerful React framework that enables you to build full-stack web applications by extending React's capabilities with server-side rendering, routing, and optimization features. In this lesson, we'll explore why Next.js has become the preferred choice for production React applications and understand its core architecture.
Next.js is a React framework for building full-stack web applications. Created by Vercel, it provides a production-ready framework that solves many common challenges faced when building React applications. Think of it as React supercharged with additional features that make development faster, applications faster, and deployment simpler.
While React is a library for building user interfaces, Next.js is a complete framework that provides:
In traditional React applications, the browser receives a minimal HTML file with a JavaScript bundle. React then runs in the browser, fetches data, and renders the content.
How it works:
Pros:
Cons:
With SSR, the server generates the full HTML for each request, including the data, and sends a complete page to the browser.
How it works:
Pros:
Cons:
SSG pre-renders pages at build time, creating static HTML files that can be served instantly from a CDN.
How it works:
Pros:
Cons:
ISR combines the benefits of SSG with the ability to update static pages after deployment.
How it works:
Pros:
The App Router is the new routing system in Next.js that introduces several key concepts:
Unlike traditional React, components in the App Router are Server Components by default:
// This is a Server Component (default)
export default function HomePage() {
// You can use async/await directly
const data = await fetch('https://api.example.com/data')
const posts = await data.json()
return (
<div>
<h1>Latest Posts</h1>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
<ClientComponent />
</div>
)
}
// This is a Client Component
'use client'
import { useState } from 'react'
export default function ClientComponent() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
Your folder structure defines your routes:
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── about/
│ └── page.tsx # About page (/about)
├── blog/
│ ├── page.tsx # Blog listing (/blog)
│ └── [slug]/
│ └── page.tsx # Blog post (/blog/my-post)
└── api/
└── posts/
└── route.ts # API endpoint (/api/posts)
Layouts are shared UI components that preserve state and remain interactive during navigation:
// app/layout.tsx - Root layout
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<nav>Navigation</nav>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
// app/blog/layout.tsx - Blog-specific layout
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="blog-layout">
<aside>Blog sidebar</aside>
<section>{children}</section>
</div>
)
}
Next.js provides several ways to fetch data, each optimized for different use cases:
// Automatic caching by default
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
// Force fresh data on every request
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}
// Revalidate data every 60 seconds
async function getStaleData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return res.json()
}
Next.js provides an optimized Image component that automatically:
import Image from 'next/image'
export default function ProfilePage() {
return (
<div>
<Image
src="/profile.jpg"
alt="Profile"
width={500}
height={500}
priority // Load immediately
/>
</div>
)
}
Next.js automatically splits your code into smaller bundles:
// This component and its dependencies are loaded only when needed
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../components/heavy'), {
loading: () => <p>Loading...</p>,
ssr: false // Only render on client
})
export default function HomePage() {
return (
<div>
<h1>Welcome</h1>
<DynamicComponent />
</div>
)
}
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
Next.js allows you to build full-stack applications with API routes:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const posts = [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' }
]
return NextResponse.json(posts)
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Create new post logic here
return NextResponse.json(
{ message: 'Post created successfully' },
{ status: 201 }
)
}
Next.js automatically prefetches routes when they appear in the viewport:
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
{/* These links will be prefetched when visible */}
</nav>
)
}
Next.js provides built-in bundle analysis:
npm run build npm run analyze
This opens a visual representation of your bundle sizes, helping you identify optimization opportunities.
Next.js provides the perfect balance between developer experience and production performance, making it an excellent choice for most modern web applications.
In the next lesson, we'll dive deeper into JSX, props, and state management in React, building on the foundation we've established with Next.js.