Przejdź do treści
Next.js

Next.js 14 App Router - Kompletny przewodnik

Poznaj najnowsze funkcje Next.js 14 i nowy App Router. Dowiedz się, jak tworzyć wydajne aplikacje React z Server Components.

15 stycznia 2024
12 min
Zagor Digital

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!

Tworzenie Stron Internetowych

Nowoczesne strony zoptymalizowane pod konwersje

Tagi:

SEO
Marketing

Udostępnij artykuł:

ZD

Zagor Digital

Eksperci w dziedzinie marketingu internetowego i pozycjonowania lokalnego.

Tworzenie Stron Internetowych

Nowoczesne strony zoptymalizowane pod konwersje

Porady SEO co tydzien

Dolacz do newslettera i otrzymuj sprawdzone strategie pozycjonowania.