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.