Sauter au contenu

Cours 05 - Composantes client et actions serveur

Dans Next.js, toutes les composantes sont rendues du côté serveur. Cela signifie que le Javascript y est exécuté/compilé par le serveur pour retourner du html au navigateur (client).

Par défaut, les composantes peuvent donc:

  • exécuter des requêtes à une base de données;
  • faire des appels fetch sans restrictions;
  • exécuter du code de façon asynchrone;
  • le référencement par les engins de recherche est optimisé comme le contenu fait partie du html (contrairement à des cadriciels client uniquement)

et leur code est à l’abris du regard des utilisateurs.

Comme seul le html/css est envoyé au client, il est impossible par défaut de réagir aux interactions de l’utilisateur par l’ajout d’écouteurs d’événements par exemple. Afin d’ajouter une couche d’interactivité aux applications, les composantes client sont nécessaires.

Afin de déclarer une composante comme client dans Next.js, on rédige simplement la commande "use client" à la première ligne de code de la composante.

'use client';
const UneComposanteClient = () => {
const saluerLeMonde = () => {
console.log('Salut le monde!');
};
const saluerQqn = (nom) => {
console.log(`Salut ${nom}!`);
};
return (
<div>
<button onClick={saluerLeMonde}>Saluer le monde</button>
<button onClick={() => saluerQqn('Bob')}>Saluer Bob</button>
</div>
);
};
  • Le rendu de la composante continue d’être exécuté du côté serveur, mais son JavaScript est aussi envoyé avec la composante afin de procéder à son hydratation;
  • Seules les composantes client peuvent écouter les différents événements dans la page et réagir aux différentes interactions de l’utilisateur;
  • Les enfants d’une composante client sont également des composantes client. La déclaration "use client" n’y est pas nécessaire.
  • Aucune composante ayant une composante client comme “ancêtre” ne peut être une composante serveur de nouveau.
  • Une composante client ne peut pas être déclarée comme étant async.

Le hook useState doit être importé depuis le package React et permet de déclarer un getter et un setter d’une variable que l’on voudra afficher et modifier à l’intérieur de la composante.

L’utilisation du setter lancera un nouveau rendu de la composante pour mettre à jour son jsx. Comme ces getters/setters sont retournés dans un tableau, il est d’usage d’utiliser la syntaxe de décomposition pour les déclarer:

'use client';
import {useState} from 'react';
const UneComposante = (props) => {
// useState nous retourne le getter puis le setter dans un tableau
// on passe la valeur initiale en paramètre
// le getter est passé en premier dans le tableau
// le setter est passé en deuxième dans le tableau
/*
const tableauNomState = useState('Jimmy'); //on passe la valeur initiale en paramètre
const nom = tableauNomState[0]; // le getter est passé en premier dans le tableau
const setNom = tableauNomState[1]; // le setter est passé en deuxième dans le tableau
*/
// On utilisera la décomposition d'un tableau pour réduire la taille du code utilisé
const [currentNom, setNom] = useState('Jimmy'); // remplace les 3 lignes de code ci-haut
const onClickHandler = () => {
setNom('Bob');
};
return (
<div>
<h3>{currentNom}</h3>
<button onClick={onClickHandler}>Clic!</button>
</div>
);
};

Selon ce qui est à faire, le développeur sera appelé à utiliser l’une ou l’autre des deux syntaxes à suivre pour mettre l’état à jour.

Si l’utilisateur souhaite remplacer la valeur de l’état sans utiliser la valeur précédente:

'use client';
import {useState} from 'react';
const UneComposante = (props) => {
const [currentNom, setNom] = useState('Jimmy');
const onClickHandler = () => {
setNom('Bob'); // La valeur précédente, Jimmy, n'est pas nécessaire ici
};
return (
<div>
<h3>{currentNom}</h3>
<button onClick={onClickHandler}>Clic!</button>
</div>
);
};

Si l’utilisateur souhaite remplacer la valeur de l’état en utilisant la valeur précédente:

'use client';
import {useState} from 'react';
const UneComposante = (props) => {
const [compteur, setCompteur] = useState(1);
const onClickHandler = () => {
// On utilise ici la valeur précédente de l'état pour l'incrémenter
// On passe une fonction qui reçoit la valeur précédente
// et retourne la nouvelle valeur
setCompteur(currentVal => currentVal + 1);
};
return (
<div>
<h3>{compteur}</h3>
<button onClick={onClickHandler}>Clic!</button>
</div>
);
};

Les Server Actions sont des fonctions asynchrones déclarées du côté server et portant la mention "use server". Cette mention ouvre la porte entre le client et le serveur et permet d’être invoquée sous forme de fonction.

Une fois compilé, un fetch est en réalité exécuté entre le client et le serveur. Ce concept n’existe que pour améliorer l’expérience de développement.

  • Si l’action serveur est déclarée directement dans la composante serveur, on ajoutera un 'use server' directement à l’intérieur de la fonction.
  • Si l’action est plutôt déclarée dans un fichier dédié, on ajoutera la mention ‘use server’ en tête de fichier.
// Lorsque l'action est déclarée directement dans la composante serveur
const ComposanteServeur = () => {
const uneAction = async () => {
'use server';
// fait qqc du côté serveur
};
return (
<form action={uneAction}>
{/* */}
</form>
);
};
// Lorsque l'action est déclarée dans un fichier dédié
// situé dans /src/app/_actions/uneAction.js
'use server'
export const uneAction = async () => {
'use server';
// fait qqc du côté serveur
};
// situé dans /src/app/_components/Form.jsx
import {uneAction} from '@/src/app/_actions/uneAction';
const Form = () => {
return (
<form action={uneAction}>
{ /* */}
</form>
);
};

Ces fonctions peuvent également être passées sous forme de props aux composantes client ou importées directement dans le fichier de la composante client. Elles peuvent être appelées par un form-action ou directement sur un gestionnaire d’événement.

Les fonctions serveur (fonctions serveur non liés à un formulaire) peuvent également être appelés depuis n’importe quelle autre exécution du côté client par un appel asynchrone et en recevoir les données retournées par la fonction.

'use client';
import {uneAction} from '@/src/app/_actions/uneAction';
const MaComposante = () => {
const clickHandler = async () => {
const result = await uneAction('test');
// en supposant que l'action retourne un objet contenant potentiellement une erreur
if (result?.error) {
alert(result.error);
return;
}
console.log('RÉUSSI!');
};
return (
<div>
<button onClick={clickHandler}>Faire des choses</button>
</div>
);
};

Il est d’usage courant de forcer un nouveau rendu de la page ou rediriger l’utilisateur vers un url différent une fois une modification de la base de données exécutées.

Ces deux opérations sont souvent exécutées à la fin d’une action serveur, mais la redirection pourrait simplement être exécutée depuis une composante serveur:

Documentation

'use server';
import {redirect} from 'next/navigation';
const uneAction = async () => {
// fait un appel d'une fonction de la couche de données
redirect('/une/autre/page');
};

Une fois une modification opérée sur la base de données (INSERT/UPDATE/DELETE), on voudra habituellement forcer un nouveau rendu de la page (et la diffuser vers le client) à l’aide de revalidatePath. Documentation

'use server';
import { revalidatePath } from "next/cache";
const uneAction = async () => {
// fait un appel d'une fonction de la couche de données
revalidatePath('/url/de/la/page');
};