Ajout et suppression d’éléments de l’historique

Ajoute des films/séries à l’historique et supprime-les avec Server Actions.

5 min read

Accéder gratuitement à cette formation

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 :

  • Le modèle Prisma et la migration
  • Les Server Actions pour l’historique
  • Les boutons client (AddHistoryItemButton, RemoveHistoryItemButton)
  • La liste d’historique avec useOptimistic et useTransition
  • La revalidation forcée via revalidatePath

Modèle Prisma et migration

On définit un modèle HistoryItem dans schema.prisma :

schema.prisma
1
model HistoryItem {
2
id Int @id @default(autoincrement())
3
user User @relation(fields: [userId], references: [id])
4
userId Int
5
movieId Int?
6
serieId Int?
7
createdAt DateTime @default(now())
8
}

Puis on génère la migration :

Terminal
1
npx prisma migrate dev --name add-history-item

Server Actions pour l’historique

On crée des actions serveur pour ajouter, lister et supprimer un item d’historique.

app/server/history.ts
1
'use server';
2
3
import { prisma } from './db';
4
import { requireUser } from './session';
5
import { getMovie, getSerie } from './movies';
6
7
// Ajouter un élément à l’historique
8
export async function addHistoryItem({
9
itemId,
10
type,
11
}: {
12
itemId: number;
13
type: 'movie' | 'serie';
14
}) {
15
'use server' // ^? indique une Server Action
16
const user = await requireUser();
17
18
return await prisma.historyItem.create({
19
data: {
20
user: { connect: { id: user.id } },
21
...(type === 'movie' && { movieId: itemId }),
22
...(type === 'serie' && { serieId: itemId }),
23
},
24
});
25
}
26
27
// Récupérer l’historique complet de l’utilisateur
28
export async function getHistory() {
29
'use server'
30
const user = await requireUser();
31
return await prisma.historyItem.findMany({
32
where: { userId: user.id },
33
orderBy: { createdAt: 'desc' },
34
});
35
}
36
37
// Supprimer un élément de l’historique
38
export async function removeHistoryItem({
39
historyItemId,
40
}: {
41
historyItemId: number;
42
}) {
43
'use server'
44
await requireUser();
45
46
// suppression en base
47
await prisma.historyItem.delete({
48
where: { id: historyItemId },
49
});
50
51
revalidatePath('/history') // ^? force la revalidation côté serveur
52
}

Composants client : add & remove

On va créer deux boutons interactifs qui appellent nos Server Actions depuis le client.

app/components/AddHistoryItemButton.tsx
1
'use client';
2
import { addHistoryItem } from '../server/history';
3
4
export function AddHistoryItemButton({
5
itemId,
6
type,
7
}: {
8
itemId: number;
9
type: 'movie' | 'serie';
10
}) {
11
return (
12
<button
13
onClick={() => addHistoryItem({ itemId, type })}
14
className="btn-add"
15
>
16
➕ Ajouter à l’historique
17
</button>
18
);
19
}
app/components/RemoveHistoryItemButton.tsx
1
'use client';
2
import { useTransition } from 'react';
3
import { removeHistoryItem } from '../server/history';
4
5
export function RemoveHistoryItemButton({
6
historyItemId,
7
removeOptimistic,
8
}: {
9
historyItemId: number;
10
removeOptimistic: (id: number) => void;
11
}) {
12
const [isPending, startTransition] = useTransition();
13
14
return (
15
<button
16
disabled={isPending}
17
onClick={() => {
18
startTransition(() => {
19
// suppression optimiste
20
removeOptimistic(historyItemId) // ^? mise à jour UI en local
21
// appel réel
22
removeHistoryItem({ historyItemId })
23
});
24
}}
25
className="btn-remove"
26
>
27
🗑 Supprimer
28
</button>
29
);
30
}

La liste d’historique et UI optimiste

Le composant HistoryList affiche l’historique, groupé par date, et gère la suppression optimiste :

app/history/HistoryList.tsx
1
'use client';
2
import { Suspense, useOptimistic } from 'react';
3
import { HistoryItem } from '../server/history';
4
import { HistoryMediaItem } from './HistoryMediaItem';
5
6
export function HistoryList({
7
initialItems,
8
}: {
9
initialItems: HistoryItem[];
10
}) {
11
// optimiste : on stocke en local et on filtre
12
const [items, removeOptimistic] = useOptimistic<
13
HistoryItem[],
14
number
15
>(initialItems, (list, id) => list.filter(i => i.id !== id));
16
17
return (
18
<Suspense fallback={<div>Chargement...</div>}>
19
{items.length === 0 ? (
20
<p>Aucun contenu dans ton historique.</p>
21
) : (
22
items.map(item => (
23
<HistoryMediaItem
24
key={item.id}
25
item={item}
26
removeOptimistic={removeOptimistic}
27
/>
28
))
29
)}
30
</Suspense>
31
);
32
}

Chaque HistoryMediaItem délègue à MovieCard ou SerieCard :

app/history/HistoryMediaItem.tsx
1
import { MovieCard } from '../components/MovieCard';
2
import { SerieCard } from '../components/MovieCard';
3
4
export function HistoryMediaItem({
5
item,
6
removeOptimistic,
7
}: {
8
item: HistoryItem;
9
removeOptimistic: (id: number) => void;
10
}) {
11
if (item.movieId) {
12
return (
13
<MovieCard
14
action="remove"
15
historyItemId={item.id}
16
removeHistoryItemOptimisticly={removeOptimistic}
17
movieId={item.movieId}
18
/>
19
);
20
}
21
if (item.serieId) {
22
return (
23
<SerieCard
24
action="remove"
25
historyItemId={item.id}
26
removeHistoryItemOptimisticly={removeOptimistic}
27
serieId={item.serieId}
28
/>
29
);
30
}
31
return null;
32
}

Révalidation forcée avec revalidatePath

Après la suppression, on veut s’assurer que le cache du RSC est mis à jour :

app/server/history.ts
1
export async function removeHistoryItem({ historyItemId }) {
2
'use server';
3
// ...
4
await prisma.historyItem.delete({ where: { id: historyItemId } });
5
revalidatePath('/history') // ^? invalide la route history côté cache
6
}

Cela déclenche le rechargement du data-fetching pour la page /history.

En résumé

  • On a défini un modèle HistoryItem et marché la migration
  • On a exposé 3 Server Actions : addHistoryItem, getHistory, removeHistoryItem
  • On a construit deux boutons React Client pour appeler ces actions
  • On a implémenté une liste avec useOptimistic pour une UI réactive
  • On a forcé la revalidation du cache avec revalidatePath

Tu maîtrises maintenant l’ajout/suppression dans l’historique avec une UX fluide et sécurisée !


Exercices

  1. Ajouter un Loader
    Ajoute un état visuel (spinner ou skeleton) sur le bouton d’ajout tant que l’action addHistoryItem est en cours.

  2. Regrouper par mois
    Modifie HistoryList pour afficher les items groupés non pas par jour, mais par mois (ex. “Janvier 2024”).

  3. 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).