Next.js 14: Server Components und App Router

  • 15 Jan 2026
  • admin
  • 5 min
  • 630

Next.js 14: Die React-Revolution

Next.js 14 führt ein neues Paradigma mit dem App Router und React Server Components ein und verändert grundlegend, wie wir React-Anwendungen erstellen.

1. App Router Grundlagen

// app/layout.tsx - Root-Layout
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="de">
      <body>
        <nav>{/* Navigation */}</nav>
        {children}
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  )
}

// app/page.tsx - Startseite (Server Component standardmäßig)
export default async function HomePage() {
  // Daten direkt in der Komponente abrufen
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // ISR: Revalidierung jede Stunde
  }).then(res => res.json())

  return (
    <main>
      <h1>Willkommen</h1>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </main>
  )
}

// app/blog/[slug]/page.tsx - Dynamische Route
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

2. Server vs Client Components

// Server Component (Standard) - Auf dem Server ausgeführt
// app/components/PostList.tsx
import { db } from '@/lib/db'

export default async function PostList() {
  // Direkter Datenbankzugriff
  const posts = await db.post.findMany()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

// Client Component - Im Browser ausgeführt
// app/components/LikeButton.tsx
'use client'

import { useState } from 'react'

export default function LikeButton({ postId }: { postId: number }) {
  const [likes, setLikes] = useState(0)
  const [isLiked, setIsLiked] = useState(false)

  const handleLike = async () => {
    const res = await fetch(\`/api/posts/\${postId}/like\`, {
      method: 'POST'
    })
    const data = await res.json()
    setLikes(data.likes)
    setIsLiked(true)
  }

  return (
    <button onClick={handleLike} disabled={isLiked}>
      {isLiked ? '❤️' : '🤍'} {likes}
    </button>
  )
}

// Komposition: Server Component mit Client Component
// app/blog/[slug]/page.tsx
import PostList from '@/components/PostList'
import LikeButton from '@/components/LikeButton'

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
      <LikeButton postId={post.id} /> {/* Client Component */}
    </article>
  )
}

3. Server Actions

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { db } from '@/lib/db'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // Validierung
  if (!title || title.length < 3) {
    return { error: 'Titel zu kurz' }
  }

  // In Datenbank speichern
  const post = await db.post.create({
    data: { title, content }
  })

  // Cache-Revalidierung
  revalidatePath('/blog')

  // Weiterleitung
  redirect(\`/blog/\${post.slug}\`)
}

export async function deletePost(id: number) {
  await db.post.delete({ where: { id } })
  revalidatePath('/blog')
}

// Verwendung in einem Formular
// app/blog/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <button type="submit">Veröffentlichen</button>
    </form>
  )
}

4. Loading und Streaming

// app/blog/loading.tsx - Automatische Loading-UI
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-5/6" />
    </div>
  )
}

// Streaming mit Suspense
import { Suspense } from 'react'

export default function BlogPage() {
  return (
    <main>
      <h1>Blog</h1>

      <!-- Sofortiges Laden -->
      <FeaturedPost />

      <!-- Streaming: später laden -->
      <Suspense fallback={<PostsSkeleton />}>
        <RecentPosts />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </main>
  )
}

// app/blog/error.tsx - Error Boundary
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Etwas ist schiefgelaufen!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Erneut versuchen</button>
    </div>
  )
}

5. Caching und Revalidation

// Fetch mit Caching
async function getData() {
  // Unbegrenzter Cache (Standard)
  const data = await fetch('https://api.example.com/data')

  // Kein Cache
  const freshData = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  })

  // Zeitbasierte Revalidierung (ISR)
  const isrData = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // 1 Stunde
  })

  // Tag-basierte Revalidierung
  const taggedData = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }
  })

  return data.json()
}

// On-Demand-Revalidierung
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'

export async function POST(request: Request) {
  const { tag, path } = await request.json()

  if (tag) {
    revalidateTag(tag)
  }

  if (path) {
    revalidatePath(path)
  }

  return Response.json({ revalidated: true })
}

6. Middleware und Route Handler

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Auth-Prüfung
  const token = request.cookies.get('token')

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Header hinzufügen
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'value')

  return response
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*']
}

// app/api/posts/route.ts - Route Handler
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const page = searchParams.get('page') || '1'

  const posts = await db.post.findMany({
    skip: (parseInt(page) - 1) * 10,
    take: 10
  })

  return NextResponse.json(posts)
}

export async function POST(request: Request) {
  const body = await request.json()
  const post = await db.post.create({ data: body })
  return NextResponse.json(post, { status: 201 })
}

Best Practices

  • Verwenden Sie Server Components standardmäßig, Client nur wenn nötig
  • Nutzen Sie Caching mit angemessener Revalidierung
  • Verwenden Sie Suspense für Content-Streaming
  • Implementieren Sie loading.tsx und error.tsx für bessere UX
  • Bevorzugen Sie Server Actions gegenüber API-Routes für Mutationen

Fazit

Next.js 14 mit App Router und Server Components repräsentiert die Zukunft der React-Entwicklung. Beherrschen Sie diese Konzepte, um schnelle und skalierbare Anwendungen zu erstellen.

Teilen Sie diesen Artikel

Kunden

Noch keine Kommentare. Seien Sie der Erste!

Einen Kommentar hinterlassen

Wird nicht veröffentlicht

Verwandte Artikel