UI optimiste et revalidation des données

Utilise useOptimistic, useTransition et revalidatePath pour une UX réactive.

4 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 fluidifier l’expérience utilisateur en appliquant une UI optimiste pour la suppression d’éléments de l’historique, et comment forcer la revalidation des données côté serveur après une mutation. Au programme :

  • Pourquoi opter pour une UI optimiste
  • Mise en place de useOptimistic dans HistoryList
  • Gestion de la mutation côté client avec useTransition
  • Forcer la revalidation via revalidatePath
  • Points clés et exercices pratiques

Pourquoi une UI optimiste ?

Lorsque l’utilisateur supprime un élément de son historique, un aller-retour vers le serveur peut introduire un délai perceptible.
Avec une UI optimiste, on met à jour l’interface immédiatement, avant même que le serveur ait confirmé la suppression.
Le résultat : une application plus réactive et un ressenti utilisateur sans latence.


1. useOptimistic dans le composant HistoryList

Le conteneur client HistoryList reçoit la liste initiale et instancie useOptimistic.
Il fournit une fonction de mise à jour optimiste qui filtre l’item supprimé avant la réponse réelle.

app/history/HistoryList.tsx
1
'use client'
2
import { Suspense, useOptimistic } from 'react'
3
import { getHistory } from '../server/history'
4
import { HistoryMediaItem } from './HistoryMediaItem'
5
6
export function HistoryList({
7
initialHistoryItems,
8
user,
9
}: {
10
initialHistoryItems: Awaited<ReturnType<typeof getHistory>>
11
user: { email: string }
12
}) {
13
// on stocke la liste et la fonction de suppression optimiste
14
const [historyItems, removeOptimistic] = useOptimistic<
15
Awaited<ReturnType<typeof getHistory>>,
16
string
17
>(
18
initialHistoryItems,
19
(current, removedId) => current.filter(item => item.id !== removedId)
20
)
21
22
return (
23
<div>
24
<h1>Watch History</h1>
25
<ul>
26
{historyItems.map(item => (
27
<HistoryMediaItem
28
key={item.id}
29
item={item}
30
user={user}
31
removeHistoryItemOptimisticly={removeOptimistic}
32
/>
33
))}
34
</ul>
35
</div>
36
)
37
}
  • initialHistoryItems : données chargées au rendu serveur
  • historyItems : état local mis à jour immédiatement
  • removeOptimistic : fonction qu’on appelle avant la requête réelle

2. Mutation côté client avec useTransition

Dans chaque carte (movie ou serie), le bouton de suppression déclenche deux actions :

  1. On applique la suppression optimiste en appelant removeOptimistic(itemId).
  2. On engage la requête serveur via removeHistoryItem({ historyItemId }).

Le hook useTransition permet de désactiver le bouton pendant la mutation.

app/components/RemoveHistoryItemButton.tsx
1
'use client'
2
import { useTransition } from 'react'
3
import { removeHistoryItem } from '../server/movies'
4
5
export function RemoveHistoryItemButton({
6
historyItemId,
7
removeHistoryItemOptimisticly,
8
}: {
9
historyItemId: string
10
removeHistoryItemOptimisticly: (id: string) => void
11
}) {
12
const [isPending, startTransition] = useTransition()
13
14
return (
15
<button
16
disabled={isPending}
17
onClick={() => {
18
startTransition(() => {
19
// 1. UI optimiste
20
removeHistoryItemOptimisticly(historyItemId)
21
// 2. appel serveur
22
removeHistoryItem({ historyItemId })
23
})
24
}}
25
>
26
{isPending ? 'Suppression…' : 'Supprimer'}
27
</button>
28
)
29
}

Ici, on ne bloque pas l’UI, on limite seulement la ré-appui jusqu’à retour réseau.


3. Revalidation côté serveur avec revalidatePath

Après suppression réelle, Next.js peut servir des données mises en cache.
Pour s’assurer que la page /history se rafraîchit, on appelle revalidatePath('/history').

app/server/movies.ts
1
'use server'
2
import { prisma } from './db'
3
import { requireUser } from './session'
4
import { revalidatePath } from 'next/cache'
5
6
export async function removeHistoryItem({
7
historyItemId,
8
}: {
9
historyItemId: string
10
}) {
11
await requireUser()
12
13
// suppression en base
14
await prisma.historyItem.delete({
15
where: { id: historyItemId },
16
})
17
18
// force la revalidation de la route /history
19
revalidatePath('/history')
20
}
app/server/movies.ts
1
- // suppression en base
2
- await prisma.historyItem.delete({ where: { id: historyItemId } })
3
+ // suppression en base
4
+ await prisma.historyItem.delete({ where: { id: historyItemId } })
5
+ // ^? Forcer une réactualisation du cache
6
7
- // retour implicite
8
+ revalidatePath('/history')

Points clés

  • useOptimistic permet de mettre à jour immédiatement l’UI
  • startTransition coordonne l’optimisme et la mutation serveur
  • revalidatePath invalide le cache Next.js pour des données toujours fraîches
  • L’optimisme doit pouvoir revenir en arrière en cas d’erreur réseau (à prévoir)

Exercices rapides

  1. Ajouter l’optimisme à la création
    • Dans AddHistoryItemButton, utilise useOptimistic pour insérer l’item avant le retour serveur.
  2. Gérer l’échec réseau
    • Simule une erreur côté removeHistoryItem et restaure l’élément dans l’UI.
  3. Revalidation conditionnelle
    • Apporte revalidateTag sur un tag personnalisé plutôt que sur un chemin fixe.

Passe à l’action et amuse-toi bien !