Der Next.js App Router hat eine Reihe nützlicher Features eingeführt, wie z.B. Unterstützung für React Server Components, besseres Caching, Streaming Responses und verschachtelte Layouts. Diese Funktionen ermöglichen es Entwicklern, eine verbesserte Benutzererfahrung zu schaffen und gleichzeitig eine bessere Entwicklererfahrung zu genießen. Auch wenn diese Funktionen hilfreich und benutzerfreundlich sind, können sie bei falscher Anwendung zu Fehlern oder Performanceproblemen führen.
In diesem Artikel gehen wir auf die häufigsten Fehler ein, die man mit dem Next.js App Router machen kann. Außerdem werden wir Lösungen mit Codebeispielen vorstellen, um diese Fehler zu entschärfen und das Benutzererlebnis zu verbessern.
Beachten Sie, dass die Beispiele in diesem Artikel die Next.js /src/app
Projektstruktur verwenden.
Das Abrufen von Daten auf der Client-Seite ist in React seit vielen Jahren gängige Praxis. So kann man leicht in die Falle tappen, redundante Netzwerkanfragen auf dem Client Komponente zu stellen , was aufgrund der zusätzlichen Netzwerklatenz und der Notwendigkeit, verschiedene HTTP-Routenhandler zu erstellen, ineffizient ist.
Im folgenden Beispiel holt die <UserComponent>
Daten auf der Client-Seite mit dem useEffect
-Hook ab. Dieser Ansatz funktioniert zwar, ist aber nicht ideal, da er mehr Netzwerkanfragen auf der Client-Seite erzeugt und zu einer langsameren Benutzererfahrung führen kann.
"use client";
import { useEffect, useState } from "react";
const UserComponent = () => {
const [data, setData] = useState<{ name: string; email: string } | null>(
null
);
useEffect(() => {
const fetchData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const result = await res.json();
setData(result);
};
fetchData();
}, []);
if (!data) return <div>Lädt...</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponent;
Um dies zu beheben, können Sie eine Server Komponente erstellen , die Daten auf der Serverseite abruft und sie in der Komponente UserComponentFixed
auf dem Server rendert:
const fetchUser = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Fehler beim Nutzerdatenaufruf");
return res.json();
};
const UserComponentFixed = async () => {
const data = await fetchUser(); // Datenaufruf auf Server-Seite
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponentFixed;
Dadurch wird sichergestellt, dass der Client ein HTML-Markup erhält, was die Belastung des Clients beim Abrufen und Rendern der Daten verringert und somit die allgemeine Benutzerfreundlichkeit verbessert.
Der Next.js App Router implementiert einen Static-First-Ansatz, was bedeutet, dass Seiten immer als statisch gerendert werden, sofern sie nicht explizit dynamische APIs verwenden. Während dieser Ansatz die Performance der Anwendung verbessert, kann er Probleme verursachen, indem er die Seite fälschlicherweise als statisch identifiziert, wenn Sie keine dynamischen APIs verwenden, aber die Seite dynamisch rendern wollen.
Im folgenden Beispiel holt die Serverkomponente Home
bei jeder Anforderung zufällige Benutzerdaten ab und zeigt sie auf der Seite an:
// src/app/page.tsx
export function random(min: number, max: number) {
return Math.floor(Math.random() * (max - min) + min);
}
export const fetchUser = async (id: number) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/" + id);
if (!res.ok) throw new Error("Fehler beim Nutzerdatenaufruf");
const data = await res.json();
return { ...data };
};
export default async function Home() {
const userId = random(1, 10);
const data = await fetchUser(userId);
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
Während der Entwicklungsphase generiert die Home-Komponente
bei jedem Rendering eine neue Zufallszahl. Im Produktions-Build, das nach dem Ausführen von next build
erstellt wird, wird die Seite jedoch statisch generiert, so dass sie stets die Details für denselben Benutzer anzeigt. Dies ist nicht das erwartete Verhalten.
Um dieses Problem zu beheben, können Sie die Verbindungsfunktion verwenden , um die Seite explizit als dynamische Seite zu rendern. Die Seite wird dann bei jedem Neuladen zufällige Benutzerdaten darstellen:
// src/app/page.tsx
import { connection } from "next/server";
export function random(min: number, max: number) {
return Math.floor(Math.random() * (max - min) + min);
}
export const fetchUser = async (id: number) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/" + id);
if (!res.ok) throw new Error("Fehler beim Nutzerdatenaufruf");
const data = await res.json();
return { ...data };
};
export default async function Home() {
await connection();
const userId = random(1, 10);
const data = await fetchUser(userId);
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
Mit Server-Aktionen können Sie von den Server- und Client-Komponenten aus Funktionen auf dem Server ausführen, z. B. für die Übermittlung von Formularen und die Änderung von Daten.
Während eine Serveraktion direkt innerhalb einer Serverkomponente erstellen werden kann, ist dies bei Clientkomponenten nicht möglich. Um Serveraktionen sicher in Client-Komponenten zu definieren und zu verwenden, müssen Serveraktionen in einer separaten Datei definiert und in die Client-Komponenten importieren werden. Auf diese Weise stellen Sie sicher, dass die Serveraktion nur auf dem Server ausgeführt wird und der Code nicht in das Client-Bundle aufgenommen wird.
Wenn Sie eine Client-Komponente für ein To-Do-Element erstellen, ist es sinnvoll, eine separate Datei für die Server-Aktion zu erstellen und sie in die Client-Komponente zu importieren, wie im folgenden Beispiel gezeigt:
"use server";
// src/app/actions.ts
export async function completeTodo(id: string) {
// Server Aktion welche das Todo schließt
}
"use client";
// src/components/TodoItem.tsx
import { completeTodo } from "@/app/actions";
export function TodoItem() {
const id = "todo_23";
return <button onClick={() => completeTodo(id)}>Geschlossen</button>;
}
Mit React Suspense können Sie einen Ladezustand anzeigen, wenn Sie auf eine Antwort warten. Aber Sie müssen auch wissen, wo Sie die Suspense Grenzen platzieren müssen, um sicherzustellen, dass sie die Performance der Anwendung verbessern und ein gutes Benutzererlebnis bieten.
Im folgenden Beispiel wird eine Spannungsgrenze auf der gesamten Seite gesetzt:
// app/page.tsx
import { Suspense } from "react";
import UserProfile from "./components/UserProfile";
import RecentPosts from "./components/RecentPosts";
export default function Page() {
return (
<Suspense fallback={<div>Seite lädt...</div>}>
<UserProfile />{" "}
{/* Beide Komponenten werden verschoben wenn eine noch lädt*/}
<RecentPosts />
</Suspense>
);
}
Dadurch wird das Rendering blockiert, bis alle Komponenten der Seite fertig geladen sind. Es kann auch zu langsamen Ladezeiten führen und sich negativ auf die Performance der Anwendung auswirken.
Es ist sinnvoller, die Suspense-Grenze auf jede Komponente zu legen, die Daten lädt:
// app/page.tsx
import { Suspense } from "react";
import UserProfile from "./components/UserProfile";
import RecentPosts from "./components/RecentPosts";
export default function Page() {
return (
<div>
<h1>Wilkommen zum Dashboard</h1>
{/* Suspense boundaries um jeden Abschnitt*/}
<Suspense fallback={<div>Profil lädt...</div>}>
<UserProfile /> {/* Ladezustand für alleine für dieses Symbol angezeigt */}
</Suspense>
<Suspense fallback={<div>Lade neuste Posts...</div>}>
<RecentPosts /> {/* Zeigt einen zweiten eigenen Ladezustand für Posts */}
</Suspense>
</div>
);
}
Auf diese Weise kann der Benutzer die Dashboard-Seite mit Lade Symbolen sehen und die Komponenten werden gerendert, wenn sie geladen werden, ohne sich gegenseitig zu blockieren.
Der App Router bietet Hilfsfunktionen für den Zugriff auf Anfragedaten, wie z. B. Header und Cookies, direkt in den Route-Handlern und den Serverkomponenten. Sie können diese Funktionen jedoch nicht in den Client-Komponenten verwenden, da es sich um serverseitige Dienstprogramme handelt. Stattdessen können Sie die usePathname-
, useSearchParams-
und useParams-Hooks
in den Client-Komponenten verwenden, um auf Informationen über die aktuelle Route zuzugreifen.
Wenn Sie in der Client-Komponente auf die Anfrage-Header oder Cookies zugreifen müssen, können Sie auf diese in einer Server-Komponente zugreifen und den abgeleiteten Wert an die Client-Komponente übergeben. Im folgenden Beispiel muss die Client-Komponente UserProfile
auf den Namen des angemeldeten Benutzers zugreifen, der nur aus dem gesicherten Cookie abgerufen werden kann:
// src/components/UserProfile.tsx
"use client"; // als Client Komponente markiert
type UserProfileProps = {
userName: string;
};
export default function UserProfile({ userName }: UserProfileProps) {
// Client Komponenten Logik
return (
<div>
<h2>Nutzer Information</h2>
<p>Eingeloggt als: {userName}</p>
</div>
);
}
Um dieses Problem zu lösen, können Sie eine Serverkomponente erstellen, die den Benutzernamen aus dem Cookie abruft und ihn an die Clientkomponente weitergibt:
// src/app/page.tsx
import { cookies } from "next/headers";
import { UserProfile } from "./components/UserProfile";
export default async function Page() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
const user = await getUserFromSession(session); // eigene Funktion
return (
<div>
<UserProfile userName={user.name} />
</div>
);
}
React Context ist eine reine Client-API und wird in den Server-Komponenten nicht unterstützt. Daher würde die Verwendung von Context direkt im Root Layout nicht funktionieren, wie im folgenden Beispiel gezeigt:
// src/app/layout.tsx
import { createContext } from "react";
// createContext ist nicht in Server Components unterstützt
export const ThemeContext = createContext({});
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
);
}
Um dieses Problem zu beheben, können Sie eine Clientkomponente erstellen, die den Kontext exportiert und ihn in der Serverkomponente verwendet:
// src/components/ThemeProvider.tsx
"use client";
import { createContext } from "react";
export const ThemeContext = createContext({});
export default function ThemeProvider({
children,
}: {
children: React.ReactNode;
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// src/app/layout.tsx
import ThemeProvider from "./components/ThemeProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
Es ist besser, die Context-Provider so tief wie möglich im Anwendungsbaum zu verwenden, damit Next.js so viel wie möglich optimieren kann. Wenn also der ThemeProvider
nur in der /dashboard/settings-Route
verwendet wird, sollte er in der nächstgelegenen layout.tsx-Datei
gerendert werden, um die beste Performance zu erzielen.
Server- und Client-Komponenten arbeiten zusammen, haben aber jeweils eine spezifische Funktion. Client-Komponenten sollen für die Zustandsverwaltung und Interaktivität verwendet werden. Sie können React-Hooks innerhalb einer Client-Komponente verwenden, aber nicht innerhalb einer Server-Komponente, wie im folgenden Beispiel:
// src/components/CheckoutForm.tsx
"use client";
import { useState, useEffect } from "react";
export default function CheckoutForm() {
const [name, setName] = useState("");
useEffect(() => {
// effect Logik
}, []);
return <form>{/* interactives Formular */}</form>;
}
Serverkomponenten eignen sich am besten für das Abrufen von Daten, da sie zusätzliche Sicherheits- und Performancevorteile bieten. Sie werden auch bevorzugt, wenn Sie umfangreiche Bibliotheken für die Datenmanipulation benötigen, wie z. B. Bibliotheken für die Datenverwaltung oder das Parsen von Daten. Hier ist ein Beispiel:
// src/components/UserDetails.tsx
import { fetchUser } from "./UserAvatar";
const UserDetailsComponent = async () => {
const data = await fetchUser(5);
return (
<div>
Ohne speichern:
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserDetailsComponent;
Das folgende Beispiel demonstriert die Verwendung von Client- und Serverkomponenten auf derselben Seite:
// src/app/page.tsx
import UserDetails from "./components/UserDetails";
export default function Page() {
// Die Page Funktion ist eine Server Komponente
return (
<div>
<h1>Dashboard</h1>
{/* Einbindung einer Client Komponente für den dynamic State */}
<CheckoutForm />
<UserDetails />
</div>
);
}
Die neueste Next. js-Version unterstützt auch die alte Pages-Router-Verzeichnisstruktur, um einen reibungslosen Übergang für bestehende Projekte zu ermöglichen, die nach und nach die App-Router-Features übernehmen wollen.
Die Funktion getServerSideProps
wird verwendet, um die Daten auf der Serverseite zu holen und das Ergebnis an die Seitenkomponente zu übergeben. Bei der Migration kann es also verlockend sein, die Funktion in einen Route-Handler umzuwandeln und sie über einen Fetch
-API-Aufruf zu konsumieren. Durch das Hinzufügen von React Server Components können Sie jedoch den serverseitigen Code zum Abrufen von Daten direkt in einer Serverkomponente platzieren.
Im folgenden Beispiel wird die Funktion getServerSideProps
verwendet, um Daten auf der Serverseite zu holen und sie an die Seitenkomponente Orders
weiterzugeben, und die Komponente Orders
wird dann verwendet, um die Daten auf der Clientseite zu rendern:
// pages/orders.js (Pages Router)
import React from "react";
export default function Orders({ orders }) {
return (
<div>
<h1>Deine Bestellungen</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Bestellung #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}
// Datenaufruf für die Seite mit getServerSideProps
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return {
props: {
orders,
},
};
}
Im App Router können Sie die Bestelldaten direkt in der Serverkomponente OrdersPage
abrufen und auf der Serverseite rendern:
// src/app/orders/page.tsx (App Router, Server Component)
export default async function OrdersPage() {
// direkter Datenaufruf in der Server Komponente
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return (
<div>
<h1>Deine Bestellungen</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Bestellung #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}
In ähnlicher Weise wird die Funktion getStaticPaths
im Pages Router verwendet, um eine Liste von Pfaden für eine statisch gerenderte dynamische Route zu erzeugen. Im App Router wird die Funktion generateStaticParams
verwendet, um ein Array von Params-Objekten für dynamische Routen zu generieren, wie folgt:
import PostLayout from "./components/post-layout";
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }];
}
async function getPost(params) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return post;
}
export default async function Post({ params }) {
const post = await getPost(params);
return <PostLayout post={post} />;
}
Die Verwendung von Client-Komponenten innerhalb von Server-Komponenten bietet zwar beträchtliche Möglichkeiten für die Komposition, kann aber auch dazu führen, dass die Grenzen zwischen Client- und Server-Code verwischt werden. Diese Verwischung birgt die Gefahr, dass sensibler serverseitiger Code oder Secrets versehentlich in die Client-Komponente aufgenommen werden.
Um sicherzustellen, dass dies nicht passiert, verwenden Sie immer die Anweisung use server
am Anfang der Deklarationen der Server-Aktionsfunktionen. Noch besser ist es, eine separate actions.ts-Datei
zu erstellen, die alle Server-Aktionen enthält, und die Direktive use server
auf der obersten oder Modulebene zu verwenden.
Sie können auch das server-only
npm-Paket verwenden, um ein server-only-Modul explizit zu deklarieren. Wenn Sie beispielsweise ein Datenabrufmodul mit dem Namen src/data.ts
haben, können Sie das server-only-Modul
installieren und wie folgt verwenden, um den Code aus dem clientseitigen Bundle auszuschließen, selbst wenn er versehentlich importiert wird:
import "server-only";
export async function getUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}
Im App Router können Sie API-Routen direkt erstellen, indem Sie eine neue route.ts
-Datei unter dem gewünschten Pfad anlegen. Aus dieser Datei können Sie verschiedene HTTP-Methoden exportieren, wie z. B. GET
, POST
, PUT
und andere.
Für die GET-Methode
können Sie Caching konfigurieren, indem Sie die Option dynamic
route config exportieren . Zum Beispiel wird die folgende Route zwischengespeichert und alle sechzig Sekunden neu bestätigt:
export const dynamic = "force-static";
export const revalidate = 60;
export async function GET() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
return Response.json({ data });
}
Eine Einschränkung bei der Verwendung von force-static
ist, dass die dynamischen Funktionen, wie Header
oder Cookies
, leere Werte zurückgeben, so dass Sie sie nicht verwenden können.
Die meisten API-Routen benötigen einen oder mehrere dynamische Parameter, um die angeforderte Ressource zu identifizieren. Hierfür können Sie den slug
-Parameter verwenden. Sie können Slugs auf die folgenden Arten definieren:
app/users/[slug]/route.ts
. Dieses Format passt zu /users/1
, /users/2
und so weiter.app/users/[...slug].ts
. Dieses Format fängt alle dynamischen Segmente ab, was bedeutet, dass diese Route auf /users/1
, /users/1/orders
, /users/1/orders/1
usw. passt.app/users/[[...slug]].ts
. Dies ist vergleichbar mit einem Catch-All, passt aber auch zu /users
, da dynamische Segmente optional sind.Sie können in Layouts, Seiten und Routen über die params
-Anweisung auf den slug
zugreifen. Die Seite src/app/blog/[slug]/page.tsx
kann beispielsweise wie folgt auf den slug
über die params
-Angabe zugreifen:
// src/app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>;
}
Auf ähnliche Weise können Sie auf den slug
über die Option params
in einem GET
-Routenhandler zugreifen, wie folgt:
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const slug = (await params).slug;
}
In diesem Artikel haben wir einige der häufigsten Fallstricke bei der Verwendung des Next.js App Routers behandelt und Möglichkeiten erörtert, diese zu beseitigen und die allgemeine Benutzererfahrung zu verbessern.
Wenn Sie mit Next.js-Anwendungenentwickeln , können Sie diese auf Upsun bereitstellen, das Features wie vertikale und horizontale Skalierung, Edge-Caching und Beobachtbarkeit von Haus aus bietet .