12 avril 2023
Focus sur le nouveau routeur app de Next 13
7 minutes de lecture
Dans un précédent article, nous vous avions présenté les concepts novateurs de Next : génération statique, dynamique, incrémentale…
Depuis la version 13 de Next est sortie, introduisant un nouveau routeur niché dans le dossier app
et non plus pages
.
Ce routeur embrasse le nouveau paradigme introduit par React 18 : les composants serveurs (RSC). Cette nouvelle API (encore en bêta) peut-être assez difficile à appréhender. Voici quelques points d'éclaircissement afin de mieux comprendre les zones d'ombres.
SSR vs RSC
Les deux concepts de SSR et les RSC peuvent être facilement confondus.
Afin de bien appréhender ces deux concepts, voici ce que fait le SSR :
- Rend un composant React sur le serveur (le markup) ;
- Envoie ce markup (HTML) au navigateur ;
- React est chargé dans le navigateur et ré-hydrate ce composant (sans re-créer le DOM du composant)
Cela permet donc d'éviter la génération du markup côté client et ainsi afficher une version non interactive du composant au 1er affichage.
Mais certains composants non interactifs, n'ont pas besoin d'être hydratés, comme les composants de layout. Dans ce cas, ces composants peuvent être considérés comme des composants serveurs.
Ceux-ci ont pour avantage de :
- Réduire la taille du bundle client (en n'incluant pas les dépendances JS telles que Lodash, Date-fns par exemple) ;
- Permettre d'utiliser des appels asynchrones directement dans les composants (appels HTTP, connexion à une base de données…) :
export default async function ServerComponent() {
const response = await fetch('/api/posts')
const posts = response.json()
return (
<main>
{posts.map((post) => (
<div key={post.id}>{post.name}</div>
))}
</main>
)
}
Tous les composants placés dans le dossier app sont par défaut des composants serveurs. Afin de
les déclarer comme des composants clients il faut ajouter la directive use client
:
`use client`
function ClientComponent {
const [username, setUsername] = useState();
return <input value={username} />
}
Il est impossible d'utiliser des fonctions apportant de l'interactivité (hooks, onClick…) dans un composant serveur (React retournera une erreur). Les RSC sont en général utiles pour servir des layouts qui ne demandent pas d'interactivité.
Il est donc impossible pour l'instant d'utiliser des librairies de composants telles que Chakra UI, Material UI ou plus généralement des librairies de CSS-in-JS (Styled-component, Emotion…).
Afin de tirer profit des RSC, il est recommandé de passer par des solutions "classiques" de CSS comme Tailwind CSS. Chez Premier Octet, nous l'utilisons en combinaison de Radix UI, ainsi nous pouvons :
- servir des composants RSC avec du style Tailwind (layout, shell, composants sans interactivité…) ;
- servir des composants clients avec des composants Radix UI stylisés avec Tailwind (boutons, composants avec interactivité…).
Dédupliquer avec fetch()
Contrairement au router pages
, nous pouvons désormais récupérer nos données directement dans nos composants React côté serveur. Exit la fonction getServerSideProps
, grâce au RSC, il est possible de faire des appels asynchrones directement dans le corps du composant :
// app/page.tsx
export default async function Page() {
const response = await fetch('/api/posts')
const posts = response.json()
return (
<main>
{posts.map((post) => (
<div key={post.id}>{post.name}</div>
))}
</main>
)
}
Avec le router app
, il est fortement conseillé d'utiliser fetch()
pour bénéficier du système de cache offert par Next.js. En effet, dans le contexte de l'app, fetch()
est légèrement surchargé par React et Next afin de :
- Dédupliquer les requêtes ;
- Spécifier des stratégies de cache propres à Next.js.
À noter que l'utilisation de client HTTP comme Axios, ne permet pas de bénéficier de ces mécanismes car il utilise XMLHttpRequest en coulisse.
Cacher les appels avec cache()
Il n'est pas toujours possible d'utiliser la méthode fetch() pour récupérer les données. Celles-ci peuvent être récupérées via un ORM tel que Prisma directement depuis une base de données.
Il est ainsi possible d'utiliser directement la fonction cache() exposée par React :
import { cache } from 'react'
export const getPosts = cache(async () => {
const posts = await db.posts.findMany()
return posts
})
export default async function Page() {
const posts = await getPosts(i)
return (
<main>
{posts.map((post) => (
<div key={post.id}>{post.name}</div>
))}
</main>
)
}
Le résultat de la fonction getPosts()
sera alors caché lorsqu'elle sera appelée dans un autre composant serveur. Cela permet d'éviter le props-drilling entre les composants serveurs.
Choisir sa stratégie de rendu
Avec l'ancien routeur de Next, il était possible de choisir sa stratégie de rendu au niveau des pages :
- Stratégie dynamique avec getServerSideProps ;
- Stratégie statique avec getStaticProps (et getStaticPaths).
Mais ces fonctions ont été tout bonnement supprimées du router app
… Comment gère-t-on alors le choix du rendu ?
Le nouveau routeur introduit une granularité plus fine au niveau du rendu, le choix de la stratégie peut se faire désormais à plusieurs niveaux :
- Au niveau d'un segment ;
- Au niveau d'une requête (avec
fetch()
au sein d'un segment).
Un segment est un dossier contenant les fichiers tels que page.tsx
ou layout.tsx
et représente donc une partie de l'URL :
Source: Next 13 Docs - Terminology
Par défaut, Next.js a une stratégie de cache aggressive. Sans indication de votre part, Next.js va mettre en cache l'ensemble de vos requêtes fetch().
Stratégie au niveau d'un segment
Vous pouvez exporter depuis chaque fichier page.tsx
, layout.tsx
ou route.tsx
des options permettant d'indiquer la stratégie de rendu à Next.js :
- dynamic permet de choisir entre le rendu statique ou dynamique
export const dynamic = 'auto' // Laisser Next.js choisir (il va prioriser le statique)
// export const dynamic = 'force-dynamic' équivalent de getServerSideProps
// export const dynamic = 'force-static' équivalent de getStaticProps
export default async function Page() {
const response = await fetch('/api/posts')
const posts = response.json()
return (
<main>
{posts.map((post) => (
<div key={post.id}>{post.name}</div>
))}
</main>
)
}
- dynamicParams permet d'indiquer si l'on souhaite que Next.js génère des nouvelles pages statiquement. C'est l'équivalent de l'option
fallback
dans l'ancienne méthodegetStaticPaths
export const dynamicParams = true
// export const dynamicParams = false,
export default async function Page() {
//…
}
- revalidate permet de définir le temps de cache d'une page statique. C'est l'équivalent de l'option
revalidate
dans l'ancienne méthodegetStaticProps
export const revalidate = false // Jamais revalider (statique)
// export const revalidate = 0 // Toujours revalider au rendu serveur (dynamique)
// export const revalidate = 60 // Revalider toutes les 60 secondes
export default async function Page() {
//…
}
Ces trois options sont bien sûr combinables.
Stratégie au niveau d'une requête
Il est aussi possible de définir ses stratégies de rendu plus finement au niveau de chaque requête. Pour cela, il suffit d'utiliser l'option cache
et revalidate
:
export default async function Page() {
const [staticData, dynamicData, revalidatedData] = await Promise.all([
fetch(`/api/posts`), // par défaut cache: force-cache
fetch(`/api/posts`, { cache: 'no-store' }),
fetch(`/api/posts`, {
next: { revalidate: 10 },
}),
]);
return <div>...</div>
}
Invalider les données
Lorsque vous effectuez des mutations sur vos données (une suppression sur une liste par exemple), il peut être difficile de rafraîchir les données côté client. En effet, ces données ont été chargées côté serveur dans un RSC.
Pour forcer le re-render des composants côté serveur, vous pouvez utiliser la fonction refresh()
offert par le nouveau router de Next.js :
// Attention il s'agit bien de 'next/navigation' et non 'next/router'
import { useRouter } from 'next/navigation'
const { refresh } = useRouter()
refresh()
Mais le refresh peut prendre quelques secondes le temps d'invalider le cache côté serveur. Pour pallier ce problème, vous pouvez utiliser le hook useTransition
offert par React :
import { useRouter } from 'next/navigation'
import { useTransition } from 'react'
const useTransitionRefresh = () => {
const [isRefreshing, startTransition] = useTransition()
const { refresh } = useRouter()
return {
isRefreshing,
refresh: () => {
startTransition(() => {
refresh()
})
},
}
}
export default useTransitionRefresh
Vous bénéficiez alors d'un attribut isRefreshing
qui vous permet d'ajouter un feedback côté client :
const App = () => {
const { isRefreshing, refresh } = useTransitionRefresh()
const { mutate: deletePost, isLoading } = useMutation(
'delete-post',
(id) => {
return api.delete(`/post/${id}`)
},
{
onSuccess: () => {
refresh()
},
}
)
return (
<>
<button
disabled={isRefreshing || isLoading}
onClick={() => {
deletePost(1)
}}
>
Delete
</button>
</>
)
}
Pour conclure
La version 13 de Next.js introduit donc un nouveau routeur basé sur les React Server Components (RSC) qui permet de simplifier et d'optimiser la gestion du rendu et de la récupération des données côté serveur. Cette nouvelle approche offre une meilleure granularité dans la gestion des stratégies de rendu et facilite la mise en cache des données.
Les RSC permettent d'économiser des ressources en ne chargeant pas de JavaScript inutile pour des composants non interactifs, et simplifient les appels asynchrones aux données en les intégrant directement dans les composants. Le nouveau routeur permet également de choisir la stratégie de rendu au niveau des segments et des requêtes individuelles, offrant ainsi un contrôle plus précis sur les performances de l'application.
Afin de vous familiariser avec les nouveaux concepts du routeur, je vous invite à jouer avec les différentes démos de ce super playground : https://app-dir.vercel.app/
👋