Ajoute des films/séries à l’historique et supprime-les avec Server Actions.
Renseigne ton email pour débloquer immédiatement cette formation gratuite.
Dans cette leçon, tu vas découvrir comment gérer la logique d’ajout et de suppression dans l’historique utilisateur, tout en apportant une UI optimiste et une revalidation automatique des données. On va couvrir :
AddHistoryItemButton
, RemoveHistoryItemButton
)useOptimistic
et useTransition
revalidatePath
On part du projet Next.js 15 full-stack (RSC + Server Actions + Prisma).
Le modèle HistoryItem
a déjà été ajouté et migré.
On définit un modèle HistoryItem
dans schema.prisma
:
1model HistoryItem {2id Int @id @default(autoincrement())3user User @relation(fields: [userId], references: [id])4userId Int5movieId Int?6serieId Int?7createdAt DateTime @default(now())8}
Puis on génère la migration :
1npx prisma migrate dev --name add-history-item
On crée des actions serveur pour ajouter, lister et supprimer un item d’historique.
1'use server';23import { prisma } from './db';4import { requireUser } from './session';5import { getMovie, getSerie } from './movies';67// Ajouter un élément à l’historique8export async function addHistoryItem({9itemId,10type,11}: {12itemId: number;13type: 'movie' | 'serie';14}) {15'use server' // ^? indique une Server Action16const user = await requireUser();1718return await prisma.historyItem.create({19data: {20user: { connect: { id: user.id } },21...(type === 'movie' && { movieId: itemId }),22...(type === 'serie' && { serieId: itemId }),23},24});25}2627// Récupérer l’historique complet de l’utilisateur28export async function getHistory() {29'use server'30const user = await requireUser();31return await prisma.historyItem.findMany({32where: { userId: user.id },33orderBy: { createdAt: 'desc' },34});35}3637// Supprimer un élément de l’historique38export async function removeHistoryItem({39historyItemId,40}: {41historyItemId: number;42}) {43'use server'44await requireUser();4546// suppression en base47await prisma.historyItem.delete({48where: { id: historyItemId },49});5051revalidatePath('/history') // ^? force la revalidation côté serveur52}
requireUser()
vérifie la session et lève une erreur si l’utilisateur n’est
pas connecté. Utile pour sécuriser tes Server Actions.
On va créer deux boutons interactifs qui appellent nos Server Actions depuis le client.
1'use client';2import { addHistoryItem } from '../server/history';34export function AddHistoryItemButton({5itemId,6type,7}: {8itemId: number;9type: 'movie' | 'serie';10}) {11return (12<button13onClick={() => addHistoryItem({ itemId, type })}14className="btn-add"15>16➕ Ajouter à l’historique17</button>18);19}
1'use client';2import { useTransition } from 'react';3import { removeHistoryItem } from '../server/history';45export function RemoveHistoryItemButton({6historyItemId,7removeOptimistic,8}: {9historyItemId: number;10removeOptimistic: (id: number) => void;11}) {12const [isPending, startTransition] = useTransition();1314return (15<button16disabled={isPending}17onClick={() => {18startTransition(() => {19// suppression optimiste20removeOptimistic(historyItemId) // ^? mise à jour UI en local21// appel réel22removeHistoryItem({ historyItemId })23});24}}25className="btn-remove"26>27🗑 Supprimer28</button>29);30}
On fait la suppression optimiste avant l’appel serveur. Ainsi l’UI répond immédiatement, même en cas de latence.
Le composant HistoryList
affiche l’historique, groupé par date, et gère la suppression optimiste :
1'use client';2import { Suspense, useOptimistic } from 'react';3import { HistoryItem } from '../server/history';4import { HistoryMediaItem } from './HistoryMediaItem';56export function HistoryList({7initialItems,8}: {9initialItems: HistoryItem[];10}) {11// optimiste : on stocke en local et on filtre12const [items, removeOptimistic] = useOptimistic<13HistoryItem[],14number15>(initialItems, (list, id) => list.filter(i => i.id !== id));1617return (18<Suspense fallback={<div>Chargement...</div>}>19{items.length === 0 ? (20<p>Aucun contenu dans ton historique.</p>21) : (22items.map(item => (23<HistoryMediaItem24key={item.id}25item={item}26removeOptimistic={removeOptimistic}27/>28))29)}30</Suspense>31);32}
Chaque HistoryMediaItem
délègue à MovieCard
ou SerieCard
:
1import { MovieCard } from '../components/MovieCard';2import { SerieCard } from '../components/MovieCard';34export function HistoryMediaItem({5item,6removeOptimistic,7}: {8item: HistoryItem;9removeOptimistic: (id: number) => void;10}) {11if (item.movieId) {12return (13<MovieCard14action="remove"15historyItemId={item.id}16removeHistoryItemOptimisticly={removeOptimistic}17movieId={item.movieId}18/>19);20}21if (item.serieId) {22return (23<SerieCard24action="remove"25historyItemId={item.id}26removeHistoryItemOptimisticly={removeOptimistic}27serieId={item.serieId}28/>29);30}31return null;32}
useOptimistic
prend la liste initiale et une fonction qui décrit
comment la mettre à jour en local (ici, on enlève l’élément supprimé).
Après la suppression, on veut s’assurer que le cache du RSC est mis à jour :
1export async function removeHistoryItem({ historyItemId }) {2'use server';3// ...4await prisma.historyItem.delete({ where: { id: historyItemId } });5revalidatePath('/history') // ^? invalide la route history côté cache6}
Cela déclenche le rechargement du data-fetching pour la page /history
.
HistoryItem
et marché la migrationaddHistoryItem
, getHistory
, removeHistoryItem
useOptimistic
pour une UI réactiverevalidatePath
Tu maîtrises maintenant l’ajout/suppression dans l’historique avec une UX fluide et sécurisée !
Ajouter un Loader
Ajoute un état visuel (spinner ou skeleton) sur le bouton d’ajout tant que
l’action addHistoryItem
est en cours.
Regrouper par mois
Modifie HistoryList
pour afficher les items groupés non pas par jour,
mais par mois (ex. “Janvier 2024”).
Annulation optimiste
Implémente un bouton “Annuler” qui ré-insère un élément supprimé si la
suppression échoue (tu peux simuler une erreur côté serveur).