Next.js 14 App Router - Kompletny przewodnik
Next.js 14 wprowadza rewolucyjne zmiany w sposobie tworzenia aplikacji React. Nowy App Router, Server Components i ulepszona wydajność czynią z Next.js 14 najlepszy wybór do tworzenia nowoczesnych aplikacji webowych.
Co nowego w Next.js 14?
1. Stabilny App Router
App Router, wprowadzony eksperymentalnie w Next.js 13, jest teraz stabilny i gotowy do produkcji:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pl">
<body>{children}</body>
</html>
)
}
2. Server Components domyślnie
Wszystkie komponenty w App Router są domyślnie Server Components:
// app/page.tsx - Server Component
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return (
<div>
<h1>Dane z serwera</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
3. Ulepszona wydajność
- Turbopack - szybszy bundler (beta)
- Partial Prerendering - hybrydowe renderowanie
- Optimized bundling - mniejsze bundle sizes
Struktura projektu App Router
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── globals.css # Global styles
├── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # Blog listing
│ └── [slug]/
│ └── page.tsx # Blog post
└── api/
└── posts/
└── route.ts # API endpoint
Routing w App Router
1. File-based routing
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default function BlogPost({ params, searchParams }: PageProps) {
return <h1>Post: {params.slug}</h1>
}
2. Route Groups
app/
├── (marketing)/
│ ├── about/
│ └── contact/
└── (shop)/
├── products/
└── cart/
3. Parallel Routes
// app/dashboard/@analytics/page.tsx
export default function Analytics() {
return <div>Analytics Dashboard</div>
}
// app/dashboard/@team/page.tsx
export default function Team() {
return <div>Team Dashboard</div>
}
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
{children}
{analytics}
{team}
</div>
)
}
Server vs Client Components
Server Components (domyślne)
// app/posts/page.tsx - Server Component
import { db } from '@/lib/db'
export default async function Posts() {
// Fetch data directly on server
const posts = await db.post.findMany()
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
Client Components
'use client' // Dyrektywa Client Component
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}
Data Fetching
1. Server-side fetching
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// Revalidate every hour
next: { revalidate: 3600 }
})
if (!res.ok) {
throw new Error('Failed to fetch posts')
}
return res.json()
}
export default async function Posts() {
const posts = await getPosts()
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
2. Static vs Dynamic rendering
// Static rendering (default)
export default async function StaticPage() {
const data = await fetch('https://api.example.com/static-data')
return <div>{/* content */}</div>
}
// Dynamic rendering
export const dynamic = 'force-dynamic'
export default async function DynamicPage() {
const data = await fetch('https://api.example.com/dynamic-data')
return <div>{/* content */}</div>
}
3. Streaming z Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react'
async function Analytics() {
// Slow data fetch
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Analytics Data</div>
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading analytics...</div>}>
<Analytics />
</Suspense>
</div>
)
}
API Routes
1. Route Handlers
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
// Fetch data
const posts = await fetchPosts(query)
return NextResponse.json(posts)
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Create post
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
}
2. Dynamic API Routes
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
Metadata API
1. Static Metadata
// app/about/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'O nas - Zagor Digital',
description: 'Poznaj zespół Zagor Digital',
openGraph: {
title: 'O nas - Zagor Digital',
description: 'Poznaj zespół Zagor Digital',
images: ['/og-about.jpg'],
},
}
export default function About() {
return <div>About page</div>
}
2. Dynamic Metadata
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
export default function BlogPost({ params }: Props) {
// Component implementation
}
Loading i Error States
1. Loading UI
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded mb-4"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
</div>
)
}
2. Error Handling
// app/blog/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Coś poszło nie tak!</h2>
<button onClick={() => reset()}>
Spróbuj ponownie
</button>
</div>
)
}
3. Not Found
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Post nie został znaleziony</h2>
<p>Sprawdź adres URL lub wróć do strony głównej.</p>
</div>
)
}
Middleware
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Redirect to login if not authenticated
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// Add custom header
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}
Optymalizacja wydajności
1. Image Optimization
import Image from 'next/image'
export default function Gallery() {
return (
<div>
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // Load immediately
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
</div>
)
}
2. Font Optimization
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pl" className={inter.className}>
<body>{children}</body>
</html>
)
}
3. Bundle Analysis
# Analiza bundle size
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Next.js config
})
Deployment
1. Vercel (recommended)
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
2. Docker
# Dockerfile
FROM node:18-alpine AS base
# Dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Best Practices
1. Struktura komponentów
// components/ui/Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary'
size?: 'sm' | 'md' | 'lg'
children: React.ReactNode
onClick?: () => void
}
export function Button({
variant = 'primary',
size = 'md',
children,
...props
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
{...props}
>
{children}
</button>
)
}
2. TypeScript configuration
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
3. Environment Variables
# .env.local
DATABASE_URL="postgresql://..."
NEXTAUTH_SECRET="your-secret"
NEXTAUTH_URL="http://localhost:3000"
// lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(1),
NEXTAUTH_URL: z.string().url(),
})
export const env = envSchema.parse(process.env)
Migracja z Pages Router
1. Stopniowa migracja
app/ # New App Router
├── layout.tsx
├── page.tsx
└── blog/
└── page.tsx
pages/ # Existing Pages Router
├── api/
├── _app.tsx
└── contact.tsx # Keep until migrated
2. Shared components
// components/Header.tsx - works in both routers
export function Header() {
return (
<header>
<nav>
{/* Navigation */}
</nav>
</header>
)
}
Podsumowanie
Next.js 14 z App Router oferuje:
✅ Lepszą wydajność - Server Components, streaming
✅ Prostszy routing - file-based, nested layouts
✅ Lepsze DX - TypeScript, error handling
✅ Nowoczesne API - fetch, metadata, middleware
✅ Gotowość na przyszłość - React 18 features
Czy warto migrować? Zdecydowanie tak! App Router to przyszłość Next.js i React.
Potrzebujesz pomocy z migracją do Next.js 14? Skontaktuj się z nami - pomożemy Ci w modernizacji aplikacji!