Comment designer ses composants React

Découvrez comment créer des composants React fullstack puissants et réutilisables grâces à de simples règles et bonnes pratiques.
Rappel sur l'intérêt de faire du React
On ne code pas en React parce que c'est un framework à la mode, parce que c'est simple ou compliqué, ou encore parce que c'est stylé.
Le premier intérêt lorsque l'on utilise React, c'est que les composants sont designés pour être réutilisables, que le code généré est optimisé (dans ses cycles de render, dans sa génération HTML, etc...).
Avec l'avènement de Next.js, on a commencé à faire des pages où le rendu server-side permettait de préparer en amont le DOM (le résultat HTML) avant même que le front ait besoin d'interpréter le JSX. Cette approche a plusieurs avantages, une réduction notable du chargement de la page côté front (on va juste rafraîchir les composants et écouter les événements du lifecycle), mais aussi apporte des améliorations pour la SEO.
En revanche, la plupart des projets Next.js où je suis intervenu avaient souvent un problème de conception : les composants ne sont pas designés pour le server-side rendering.
L'approche React-Query
Il y a plusieurs façons d'exploiter Next.js sur les projets. Certains développeurs utilisent Next.js uniquement pour faire des requêtes vers des API, passent les réponses au client sous forme déshydratée (ou sérialisée) afin d'utiliser ensuite React-Query pour récupérer ce "cache" côté front.
// Composant back
import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"
export const BackendComponent = async () => {
const data = await fetch("http://api.weather.url/data")
const json = await data.json()
const queryClient = new QueryClient()
queryClient.setQueryData(["weather-data"], json)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<FrontendComponent />
</HydrationBoundary>
)
}
// Composant front
"use client"
import { useQuery } from "@tanstack/react-query"
export const FrontendComponent = () => {
const { data: myData } = useQuery({
queryKey: ["weather-data"],
queryFn: () => fetch("http://api.weather.url/data").then((res) => res.json()),
initialData: undefined
})
return (
<div>
{myData.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
Dans cet exemple, le server side ne prépare pas le DOM, c'est bien le front qui va se charger de ça. En revanche il a passé les données au front pour éviter d'ajouter le temps de latence au moment du chargement côté navigateur. Dans certains cas, ce procédé permet de garder une opacité sur les appels API (empêcher l'exposition des clés, token etc...).
Pour autant, il n'y a aucune optimisation SEO dans cet exemple. Lorsque l'on va analyser le code source de la page au chargement, on constatera que les données passées à React-Query sont en fait un JSON qui passe dans le code source et qui sera ensuite parsé côté front.
L'approche Server-Side Rendering
On peut essayer de rendre le DOM côté serveur pour éviter au front d'avoir à le faire de son côté et ajouter de la latence alors que les données sont déjà chargées. Et cette approche est aussi bénéfique pour le SEO du site, car le DOM a beaucoup plus de valeur pour le crawler que de la donnée brute dans un array. Il est possible de procéder de la sorte :
Depuis un fichier serverSideAction.ts :
"use server"
export const getDataFromServerSideAction = async () => {
const data = await fetch("http://api.weather.url/data")
const result = await data.json()
return result
}
Dans la page:
import ...
export const MyComponent = ({ item }: { item: any }) => {
return <div>{item.name}</div>
}
export const BackendPage = async () => {
const json = await getDataFromServerSideAction()
return (
<div>
{json.map((item) => (
<MyComponent key={item.id} item={item} />
))}
</div>
)
}
Dans ce cas, le composant sera rendu côté serveur.
Ce qu'il faut savoir concernant les composants
J'utilise souvent les classes en TypeScript afin d'ajouter des méthodes (utilitaires, etc...) ou champs virtuels, clés camelCase etc... au lieu de manipuler directement les réponses API ou base de données.
Cependant, si vous procédez de la sorte, il est primordial de créer des méthodes de sérialisation afin de les rendre exploitables dans vos composants JSON. En général, je crée la méthode statique "fromDatabaseObject" pour me rendre une instance de ma classe ou encore une méthode "toDatabaseObject" pour rendre un plain object au format base de données.
Le plain object, c'est l'ingrédient essentiel de vos composants afin qu'ils fonctionnent toujours en fullstack.
C'est à dire que vous ne devez passer en props, que des valeurs qui sont :
- des primitives (string, number, boolean...)
- des plain objects (un JSON)
Lorsque vous passez des classes avec des méthodes etc., vous ne pouvez pas render ce composant depuis le backend, ceci peut même lever une erreur !
Le cas des données provenant du projet
Si vous voulez récupérer en server-side les données provenant du même projet, ne repassez pas par les API du serveur Next pour fetch. Dans votre server-side action, appelez directement votre base de données.
Dans le cas contraire, vous allez créer une espèce de boucle de latence qui repasse par du network alors que le projet se parle à lui-même. Dans ce cas, vous ne faites qu'ajouter de la latence inutilement.
FAQ
Pourquoi mes composants Next.js ne fonctionnent pas bien côté serveur ?
La cause la plus fréquente est de passer des instances de classes avec des méthodes en props, ce qui est incompatible avec le server-side rendering et peut même lever une erreur. Il faut se limiter à des primitives ou des plain objects sérialisables.
React-Query côté serveur, c'est vraiment utile pour le SEO ?
Non, l'approche React-Query avec hydratation ne génère pas de DOM côté serveur : les données transitent sous forme de JSON brut dans le code source, ce que les crawlers exploitent beaucoup moins bien qu'un vrai HTML rendu. Si le SEO est un enjeu, mieux vaut opter pour un rendu serveur complet.
Comment structurer mes classes TypeScript pour qu'elles soient compatibles avec le fullstack ?
Il suffit d'ajouter des méthodes de sérialisation à tes classes, comme fromDatabaseObject pour créer une instance et toDatabaseObject pour obtenir un plain object. C'est ce plain object que tu passes ensuite en props à tes composants.
Faut-il passer par les routes API de Next.js pour récupérer ses propres données en server-side ?
Non, c'est une erreur courante : si les données viennent du même projet, appelle directement ta base de données depuis ta server-side action. Passer par le réseau pour se parler à soi-même ajoute de la latence sans aucun bénéfice.

Alexandre P.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Poursuivre la lecture


