Bonjour à tous, bienvenue au sixième segment de ma veille technologique sur Next.js.
Il s'agit de la deuxième partie de ce que nous avons vu la semaine passée, à savoir le traitement de données et les actions serveur avec Next.js.
Prérequis
Pour cette partie concernant l'ajout et la suppression de données, nous aurons besoin d'un accès à une base de données dans laquelle nous pourrons ajouter et supprimer des tâches. Nous allons donc mettre de côté l'application en ligne mockapi.io
et utiliser à la place la librairie Prisma, qui nous permet de créer facilement une base de données accessible pour notre projet Next.js.
Pour ce faire, il suffit d'installer la librairie à la racine du projet Next.js avec la commande suivante :
npx prisma@latest init --datasource-provider sqlite
Cette commande va télécharger et initialiser la dernière version de la librairie Prisma dans le projet en configurant SQLite comme source de base de données (qui requiert moins de mise en place, ce qui facilite cette démo).
Ensuite, nous allons modifier le fichier généré prisma/schema.prisma
et y ajouter notre modèle de base de données pour nos tâches :
model Tache {
id String @id @default(uuid())
nom String
description String
}
La dernière commande à exécuter pour mettre en place notre base de données est celle permettant de faire la première migration, qui créera la table pour les tâches. Il suffit d'exécuter la commande suivante :
npx prisma migrate dev --name init
Cette commande devrait générer un dossier de migrations contenant la première migration init
ainsi qu'un fichier dev.db
qui contiendra la base de données.
Pour terminer la mise en place de Prisma, nous allons créer un singleton afin de pouvoir accéder à la base de données. En effet, Next.js recompile le projet à chaque modification, ce qui peut poser problème pour le client de base de données, qui ne devrait être instancié qu'une seule fois.
Pour corriger cela, nous allons ajouter un fichier TypeScript pour ce singleton dans le dossier src
avec le contenu suivant :
import { PrismaClient } from '@prisma/client';
const globalPrisma = global as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalPrisma.prisma ?? new PrismaClient({ log: ['query'] });
if (process.env.NODE_ENV !== 'production') globalPrisma.prisma = prisma;
Dans cet extrait de code, nous ajoutons une variable prisma à l'objet global JavaScript, puis nous exportons une instance unique du client Prisma. La dernière ligne permet d'affecter cette instance à l'objet global uniquement en mode développement, afin d'éviter la réinitialisation du client lors des rechargements à chaud.
Modification des requêtes existantes
Maintenant que notre base de données est en place, nous allons modifier nos requêtes pour utiliser directement la base de données.
Dans le fichier taches/page.tsx
, remplaçons ces lignes :
const reponse = await fetch('https://67bfdc3db9d02a9f2247708c.mockapi.io/api/v1/tache');
const taches: Tache[] = await reponse.json();
par celle-ci :
const taches: Tache[] = await prisma.tache.findMany();
Dans cette requête, on utilise l'objet prisma définit précédemment pour aller chercher toutes les tâches existantes dans notre base de donnée.
De la même manière, nous allons modifier la requête pour récupérer une tâche en particulier dans taches/[id]/page.tsx
.
Remplaçons :
const reponse = await fetch(`https://67bfdc3db9d02a9f2247708c.mockapi.io/api/v1/tache/${id}`);
const tache: Tache = await reponse.json();
par :
const tache: Tache | null = await prisma.tache.findFirst({ where: { id: id } })
if (tache === null) {
redirect('/taches')
}
Dans cet extrait, on récupère la première tâche de la base de données ayant l'ID passé en paramètre de la route, puis on vérifie si la tâche retournée existe. Si ce n'est pas le cas, on redirige l'utilisateur vers la page des tâches en utilisant la fonction redirect
de Next.js, qui permet de rediriger l'utilisateur vers un autre emplacement.
Pour l'instant, comme nous n'avons pas encore ajouté de tâches, rien ne s'affiche dans la liste des tâches. Cela tombe bien, car nous allons justement mettre en place la logique d'ajout de données pour notre application Next.js et voir comment fonctionnent les interactions client-serveur lors de l'envoi de données.
Ajout de nouvelles tâches
Nous allons maintenant implémenter la création de nouvelles tâches. Pour ce faire, nous allons modifier le fichier /taches/ajouter/pages.tsx
.
Next.js permet d'assigner à l'attribut action
d'un formulaire une fonction identifiée comme étant exécutée côté serveur. Cela permet d'envoyer automatiquement la requête du formulaire ainsi que les valeurs de tous les champs lors de sa soumission.
Nous pouvons donc ajouter l'attribut action
à notre formulaire et spécifier le nom de la fonction côté serveur que nous allons créer :
<form className={styles.form} action={creerTache}>
Puis, nous allons créer la fonction juste au-dessus du composant de la page :
const creerTache = async (data: FormData) => {
"use server"
const titre = data.get("titre")?.valueOf()
const description = data.get("description")?.valueOf();
if (typeof titre !== 'string' || titre.length === 0) {
throw new Error("Le titre est invalide");
}
if (typeof description !== 'string') {
throw new Error("La description est invalide");
}
await prisma.tache.create({ data: { nom: titre, description: description } })
redirect('/taches');
}
Comme on peut le voir dans cet extrait de code, la fonction prend en paramètre un objet FormData
contenant les données envoyées par le formulaire. Nous pouvons alors effectuer les vérifications nécessaires côté serveur avant d'utiliser ces données pour créer une nouvelle tâche.
La première ligne de la fonction est essentielle : elle indique que la fonction s'exécute côté serveur. Si cette déclaration est absente, une erreur se produira lors de l'accès à la page d'ajout de tâches, car il est impossible d'assigner une fonction client à l'attribut action
d'un formulaire.
- L'attribut
"use server"
(ainsi que"use client"
) est un concept clé dans Next.js. Il permet de définir quels éléments doivent être exécutés côté client et lesquels doivent l'être côté serveur. Cela apporte à la fois de la flexibilité et de la complexité, mais permet de mieux structurer l'application en définissant précisément ce à quoi le client peut accéder.
Avec l'objet data
, nous récupérons la valeur des champs en utilisant l'attribut name
du formulaire, puis nous accédons à leur contenu avec valueOf()
.
Les deux blocs suivants servent à valider les entrées utilisateur.
Enfin, nous effectuons une requête à la base de données pour ajouter la nouvelle tâche avec les informations fournies par l'utilisateur. Ensuite, nous utilisons redirect
pour renvoyer l'utilisateur vers la liste des tâches une fois la création réussie.
Si l'on soumet le formulaire avec des champs valides, nous devrions maintenant pouvoir créer notre première tâche et la voir apparaître dans la liste des tâches.
Dans l'onglet Réseau
de l'inspecteur du navigateur, on peut observer qu'une requête est envoyée à la fonction serveur lors de la soumission du formulaire. Les données du formulaire sont incluses dans le corps de la requête. Tout cela est géré automatiquement par Next.js grâce à l'attribut "use server"
.
C'est magique !
Supprimer une tâche existante
La dernière fonctionnalité que nous allons ajouter dans cette démo est la suppression d'une tâche. Ainsi, nous aurons une petite application fonctionnelle permettant d'ajouter, consulter et supprimer des tâches.
Pour ce faire, nous allons ajouter un bouton sur la page de détails d'une tâche. Lors d'un clic, ce bouton enverra une requête à la fonction serveur responsable de la suppression de la tâche, puis redirigera l'utilisateur vers la liste des tâches.
Nous allons commencer par créer un nouveau composant pour ce bouton.
Composant BoutonSupprimerTache
'use client'
import { FC } from "react";
import styles from './bouton-supprimer-tache.module.css';
type Props = {
idTache: string;
onSuppression: (id: string) => void;
}
export const BoutonSupprimerTache: FC<Props> = ({ idTache, onSuppression }) => {
return <button className={styles.button} onClick={() => onSuppression(idTache)}>Supprimer</button>
}
Nous avons ajouté l'instruction use client
en haut du fichier, car ce composant contient des interactions utilisateur. C'est notamment nécessaire pour permettre l'événement onClick
qui déclenche la suppression de la tâche.
Ensuite, nous devons modifier la page de détails d'une tâche pour inclure ce bouton et gérer la suppression.
Mise à jour de la page des détails d'une tâche
import { Tache } from '@/types/tache';
import styles from './page.module.css';
import Link from 'next/link';
import { prisma } from '@/db/config';
import { redirect } from 'next/navigation';
import { BoutonSupprimerTache } from '@/components/bouton-supprimer-tache/bouton-supprimer-tache';
const supprimerTache = async (id: string) => {
'use server'
await prisma.tache.delete({ where: { id: id } })
redirect('/taches')
}
const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
const id = (await params).id;
const tache: Tache | null = await prisma.tache.findFirst({ where: { id: id } })
if (tache === null) {
redirect('/taches')
}
return (
<div className={styles.page}>
<h2>{tache.nom}</h2>
<p>Description</p>
<p>
{tache.description}
</p>
<Link href={'/taches'} className={styles.button}>
Retour
</Link>
<BoutonSupprimerTache idTache={id} onSuppression={supprimerTache} />
</div>
);
};
export default Page;
Dans ce fichier :
- Nous avons ajouté une fonction serveur
supprimerTache
pour supprimer une tâche en fonction de son ID. - Nous avons intégré le composant
BoutonSupprimerTache
, en lui passant l'ID de la tâche et la fonction de suppression.
Il est important de ne pas utiliser onClick
ou d'autres événements interactifs directement dans cette page, car elle est exécutée côté serveur. Toutes les interactions doivent être gérées dans un composant client, comme BoutonSupprimerTache
.
Après cette mise à jour, nous devrions voir apparaître un bouton de suppression sur la page des détails d'une tâche. Lors du clic, la tâche sera supprimée et l'utilisateur sera redirigé vers la liste des tâches.
Conclusion
Félicitations ! Vous avez créé une première application Next.js avec des fonctionnalités de base comme l'ajout, la suppression et la visualisation de données. Ces trois concepts fondamentaux vous permettront d'aller plus loin dans le développement de fonctionnalités. Bien sûr, cette série d'articles n'est qu'un entrée dans le monde de Next.js, il nous reste encore beaucoup de détails à découvrir sur le fonctionnement de ce cadre applicatif.
Lors du prochain article, je ferai d'ailleurs un compte rendu de ce que nous avons appris jusqu'à maintenant sur le fonctionnement de Next.js, et je donnerai mon avis sur mes apprentissages jusqu'à maintenant concernant ces nouveaux outils qui viennent s'ajouter à React.
Sur ce, je vous dis à la semaine prochaine !
Sources
Vercel Team, Server Actions and Mutations - Next.js Documentation, https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations (Page consultée le 4 mars 2025).
Web Dev Simplified, Learn Next.js 13 With This One Project, https://www.youtube.com/watch?v=NgayZAuTgwM (Vidéo consultée le 4 mars 2025).
Prisma Team, Prisma - Next-generation ORM for Node.js & TypeScript, https://www.prisma.io/ (Page consultée le 4 mars 2025).
Wikipedia Contributors, Singleton Pattern, https://en.wikipedia.org/wiki/Singleton_pattern (Page consultée le 4 mars 2025).
Commentaires1
Séparation
Salut Philippe,
Premièrement, merci pour ton blog c'est super intéréssant.
Ensuite, je trouve ça quand même assez fou d'avoir du code qui accède directement à la base de données dans un composant. Je ne sais pas pourquoi ça semble contre intuitif, mais en même temps c'est surement une question d''habitude. "use server/use client", c'est fou ca doit permettre de faire des applications en un temps record.