- Fonctionnalités
- Pricing

Le Next.js App Router a introduit toute une série de fonctionnalités utiles, telles que la prise en charge des composants serveur React, une mise en cache améliorée, les réponses en streaming et les mises en page imbriquées. Ces fonctionnalités permettent aux développeurs d'offrir une meilleure expérience utilisateur tout en bénéficiant d'une meilleure expérience de développement. Bien que ces fonctionnalités soient utiles et conviviales, une utilisation incorrecte peut entraîner des bugs ou des problèmes de performances.
Dans cet article, nous allons passer en revue les erreurs les plus courantes que tu peux commettre avec le Next.js App Router. Nous te présenterons également des solutions, accompagnées d'exemples de code, pour t'aider à éviter ces erreurs et à améliorer l'expérience utilisateur.
Note que les exemples de cet article utilisent la structure de projet Next.js /src/app.
La récupération de données côté client est une pratique courante dans React depuis de nombreuses années. Il est donc facile de tomber dans le piège des requêtes réseau redondantes au niveau du composant client, ce qui n’est pas très efficace en raison de la latence réseau supplémentaire et de la nécessité de créer différents gestionnaires de routes HTTP.
Dans l'exemple suivant, l'<UserComponent> récupère des données côté client à l'aide du hook `useEffect`. Bien que cette approche fonctionne, elle n'est pas idéale car elle ajoute davantage de requêtes réseau côté client et peut ralentir l'expérience utilisateur.
"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>Loading...</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponent;Pour y remédier, tu peux créer un composant serveur qui récupère les données côté serveur et les affiche dans le composant UserComponentFixed sur le serveur :
const fetchUser = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Failed to fetch user");
return res.json();
};
const UserComponentFixed = async () => {
const data = await fetchUser(); // Fetching on the server side
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponentFixed;Cela garantit que le client reçoit un balisage HTML, ce qui réduit la charge sur le client pour récupérer et afficher les données, améliorant ainsi l'expérience utilisateur globale.
Le routeur d'applications Next.js met en œuvre une approche « static-first », ce qui signifie que les pages sont toujours rendues de manière statique, sauf si elles utilisent explicitement des API dynamiques. Bien que cette approche améliore les performances de l'application, elle peut poser des problèmes en identifiant à tort la page comme statique lorsque tu souhaites la rendre de manière dynamique mais que tu n'utilises pas de déclencheurs dynamiques traditionnels.
Pour résoudre ce problème, tu dois utiliser la fonction connection pour activer explicitement le rendu dynamique. Cela garantit que la logique de la page, comme la génération d'un identifiant aléatoire ou la récupération de données récentes, s'exécute à chaque requête :
// 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("Failed to fetch user");
const data = await res.json();
return { ...data };};
export default async function Home() {
// Explicitly opt into dynamic rendering
await connection();
const userId = random(1, 10);
const data = await fetchUser(userId);
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);}
Les actions serveur te permettent d'exécuter des fonctions sur le serveur à partir des composants serveur et client pour des cas d'utilisation tels que la soumission de formulaires et les mutations de données.
Bien que tu puisses créer une action serveur directement à l'intérieur d'un composant serveur, tu ne peux pas faire de même pour les composants client. Pour définir et utiliser en toute sécurité des actions serveur à l'intérieur de composants client, tu dois définir les actions serveur dans un fichier séparé et les importer dans les composants client. En procédant ainsi, tu t'assures que l'action serveur est exécutée uniquement sur le serveur et que le code est exclu du bundle côté client.
Si tu crées un composant client de type « tâche à faire », il est judicieux de créer un fichier séparé pour l'action serveur et de l'importer dans le composant client, comme le montre l'exemple suivant :
"use server";
// src/app/actions.ts
export async function completeTodo(id: string) {
// server action to perform todo completion
}
"use client";
// src/components/TodoItem.tsx
import { completeTodo } from "@/app/actions";
export function TodoItem() {
const id = "todo_23";
return <button onClick={() => completeTodo(id)}>Complete</button>;
}React Suspense te permet d'afficher un état de chargement en attendant une réponse. Mais tu dois aussi savoir où placer les limites Suspense pour t'assurer que cela améliore les performances de l'application et offre une bonne expérience utilisateur.
L'exemple suivant définit une limite Suspense sur l'ensemble de la page :
// 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>Loading page...</div>}>
<UserProfile />{" "}
{/* Both components are delayed if one is still loading */}
<RecentPosts />
</Suspense>
);
}Cela bloque le rendu jusqu’à ce que tous les composants de la page aient fini de se charger. Cela peut aussi entraîner des temps de chargement lents et avoir un impact négatif sur les performances de l’application.
Il est plus judicieux de placer la limite de suspense sur chaque composant qui charge des données :
// app/page.tsx
import { Suspense } from "react";
import UserProfile from "./components/UserProfile";
import RecentPosts from "./components/RecentPosts";
export default function Page() {
return (
<div>
<h1>Welcome to the Dashboard</h1>
{/* Place Suspense boundaries around each section */}
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile /> {/* Shows loading state only for this component */}
</Suspense>
<Suspense fallback={<div>Loading recent posts...</div>}>
<RecentPosts /> {/* Shows a different loading state for posts */}
</Suspense>
</div>
);
}De cette façon, l'utilisateur peut voir la page du tableau de bord avec des squelettes de chargement et les composants s'afficheront au fur et à mesure de leur chargement sans se bloquer les uns les autres.
L'App Router fournit des fonctions utilitaires pour accéder aux données de requête, telles que les en-têtes et les cookies, directement dans les gestionnaires de route et les composants serveur. Mais tu ne peux pas utiliser ces fonctions dans les composants client, car ce sont des utilitaires côté serveur. À la place, tu peux utiliser les hooks usePathname, useSearchParams et useParams dans les composants client pour accéder aux informations sur la route actuelle.
Si tu as besoin d'accéder aux en-têtes de requête ou aux cookies dans le composant client, tu peux y accéder dans un composant serveur et transmettre la valeur dérivée au composant client. Dans l'exemple suivant, le composant client UserProfile doit accéder au nom de l'utilisateur connecté, qui ne peut être récupéré qu'à partir du cookie sécurisé :
// src/components/UserProfile.tsx
"use client"; // Marked as a client component
type UserProfileProps = {
userName: string;
};
export default function UserProfile({ userName }: UserProfileProps) {
// some interactive logic for the client component
return (
<div>
<h2>User Information</h2>
<p>Logged in as: {userName}</p>
</div>
);
}Pour résoudre ce problème, tu peux créer un composant serveur qui récupère le nom de l'utilisateur à partir du cookie et le transmet au composant client :
// 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); // this is a custom function
return (
<div>
<UserProfile userName={user.name} />
</div>
);
}React Context est une API réservée au client et n'est pas prise en charge dans les composants serveur. Utiliser context directement dans la mise en page racine ne fonctionnerait donc pas, comme le montre l'exemple suivant :
// src/app/layout.tsx
import { createContext } from "react";
// createContext is not supported in Server Components
export const ThemeContext = createContext({});
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
);
}Pour résoudre ce problème, tu peux créer un composant client qui exporte le contexte et l'utilise dans le composant serveur :
// 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>
);
}Il est préférable d'utiliser les fournisseurs de contexte aussi profondément que possible dans l'arborescence de l'application afin de permettre à Next.js d'optimiser autant que possible. Ainsi, si l'ThemeProvidere n'est utilisé que dans la route /dashboard/settings, affiche-le dans le fichier layout.tsx le plus proche pour obtenir les meilleures performances.
Les composants serveur et client fonctionnent en tandem, mais chacun a une fonctionnalité spécifique. Utilise les composants client pour toute gestion d'état et interactivité. Tu peux utiliser les hooks React à l'intérieur d'un composant client, mais pas à l'intérieur d'un composant serveur, comme dans l'exemple suivant :
// src/components/CheckoutForm.tsx
"use client";
import { useState, useEffect } from "react";
export default function CheckoutForm() {
const [name, setName] = useState("");
useEffect(() => {
// some effect logic
}, []);
return <form>{/* interactive form */}</form>;
}Les composants serveur sont les mieux adaptés à la récupération de données en raison des avantages supplémentaires en matière de sécurité et de performances. Ils sont également préférables lorsque tu as besoin de bibliothèques lourdes pour la manipulation de données, telles que des bibliothèques de gestion ou d'analyse de données. Voici un exemple :
// src/components/UserDetails.tsx
import { fetchUser } from "./UserAvatar";
const UserDetailsComponent = async () => {
const data = await fetchUser(5);
return (
<div>
No store:
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserDetailsComponent;L'exemple suivant illustre l'utilisation de composants client et serveur sur la même page :
// src/app/page.tsx
import UserDetails from "./components/UserDetails";
export default function Page() {
// The Page function is a server component
return (
<div>
<h1>Dashboard</h1>
{/* Including a client component to handle dynamic state */}
<CheckoutForm />
<UserDetails />
</div>
);
}La dernière version de Next.js prend également en charge l'ancienne structure de répertoires du Pages Router afin d'assurer une transition en douceur pour les projets existants qui souhaitent adopter progressivement les fonctionnalités de l'App Router.
La fonction `getServerSideProps` sert à récupérer les données côté serveur et à transmettre le résultat au composant de page. Ainsi, lors de la migration, on peut être tenté de convertir cette fonction en gestionnaire de route et de l'utiliser via un appel API `fetch`. Mais en ajoutant les composants serveur React, tu peux placer le code de récupération des données côté serveur directement à l'intérieur d'un composant serveur.
Dans l'exemple suivant, la fonction getServerSideProps est utilisée pour récupérer des données côté serveur et les transmettre au composant de page Orders, puis le composant Orders est utilisé pour afficher les données côté client :
// pages/orders.js (Pages Router)
import React from "react";
export default function Orders({ orders }) {
return (
<div>
<h1>Your Orders</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Order #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}
// Fetch data for the page with getServerSideProps
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return {
props: {
orders,
},
};
}Dans l'App Router, tu peux récupérer les données des commandes directement dans le composant serveur OrdersPage et les afficher côté serveur :
// src/app/orders/page.tsx (App Router, Server Component)
export default async function OrdersPage() {
// Fetch the data directly in the server component
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return (
<div>
<h1>Your Orders</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Order #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}De même, la fonction getStaticPaths du routeur Pages sert à générer une liste de chemins pour une route dynamique rendue de manière statique. Dans le routeur App, tu utilises la fonction generateStaticParams pour générer un tableau d’objets params pour les routes dynamiques, comme suit :
import PostLayout from "./components/post-layout";
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }];
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();
return post;
}
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return <PostLayout post={post} />;
}Si l'utilisation de composants client à l'intérieur de composants serveur offre d'importantes possibilités de composition, elle peut aussi parfois brouiller les frontières entre le code client et le code serveur. Ce flou crée un risque d'exposer involontairement du code côté serveur sensible ou des secrets au client en les incluant par erreur dans le composant client.
Pour t'assurer que cela n'arrive jamais, utilise toujours la directive `use server` en haut des déclarations de fonctions d'action serveur. Mieux encore, crée un fichier `actions.ts` séparé pour contenir toutes les actions serveur et utilise la directive `use server` en haut ou au niveau du module.
Tu peux également utiliser le package npm server-only pour déclarer explicitement un module réservé au serveur. Par exemple, si tu as un module de récupération de données appelé src/data.ts, tu peux installer le module server-only et l'utiliser comme suit pour exclure le code du bundle côté client, même s'il est importé par erreur :
import "server-only";
export async function getUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}Un changement important dans l'App Router moderne est que params et searchParams sont désormais asynchrones. Tu dois traiter ces objets comme des Promises et les attendre avant d'accéder à leurs propriétés. Les accéder de manière synchrone entraînera désormais des erreurs pendant le processus de compilation.
Tu peux accéder au slug à partir de la propriété params dans tes composants de page comme suit :
// src/app/blog/[slug]/page.tsx
// Note the async function and the Promise type for params
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
// You must await params before accessing the slug
const { slug } = await params;
return <div>My Post: {slug}</div>;
}De même, tu dois attendre les params dans tes gestionnaires de routes API :
// src/app/api/users/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
// Await the params promise to get the slug
const { slug } = await params;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${slug}`);
const data = await res.json();
return Response.json({ data });
}Dans cet article, nous avons abordé certains des pièges les plus courants lors de l'utilisation du routeur d'applications Next.js et discuté des moyens de les contourner et d'améliorer l'expérience utilisateur globale.
Si tu développes des applications Next.js, tu peux les déployer sur Upsun, qui offre des fonctionnalités telles que la mise à l'échelle verticale et horizontale, la mise en cache en périphérie et l'observabilité prêtes à l'emploi.