
useoptimistic-hook-generator
by RomualdP
Volley App Monorepo
SKILL.md
name: useOptimistic Hook Generator description: Implémente useOptimistic pour instant UI updates avec VRAIE gestion du rollback. MANDATORY pour delete operations. À utiliser lors de mutations, deletes, ou quand l'utilisateur mentionne "optimistic", "instant update", "delete", "mutation". allowed-tools: [Read, Write, Edit]
useOptimistic Hook Generator
🎯 Mission
Implémenter useOptimistic pour des mises à jour instantanées de l'UI avec gestion correcte du rollback en cas d'erreur.
⚡ Concept
useOptimistic met à jour l'UI immédiatement (optimistic), puis :
- ✅ Si succès : State synchronisé via props ou re-fetch
- ❌ Si erreur : ROLLBACK MANUEL REQUIS (pas automatique !)
🚨 ATTENTION : Rollback Manuel
ERREUR COURANTE :
// ❌ FAUX - useOptimistic ne rollback PAS automatiquement
const handleRemove = async (memberId: string) => {
removeOptimisticMember(memberId);
const result = await removeMemberAction(memberId);
// Si erreur, l'UI reste dans l'état optimiste (membre supprimé)
// = BUG !
};
useOptimistic ne gère PAS les erreurs automatiquement. Il faut gérer le rollback manuellement.
✅ Solution Recommandée : Server Component + revalidatePath
Template Complet
// ==========================================
// 1. Server Component (Page)
// ==========================================
// app/clubs/[id]/members/page.tsx
export default async function MembersPage({ params }: { params: { id: string } }) {
// Fetch data server-side
const members = await fetchMembers(params.id);
return (
<div>
<h1>Membres</h1>
<MembersList clubId={params.id} initialMembers={members} />
</div>
);
}
// ==========================================
// 2. Client Component avec useOptimistic
// ==========================================
// components/MembersList.tsx
'use client';
import { useOptimistic } from 'react';
import { removeMemberAction } from '../actions/remove-member.action';
import { toast } from 'sonner';
interface Member {
id: string;
name: string;
email: string;
}
interface MembersListProps {
clubId: string;
initialMembers: Member[];
}
export function MembersList({ clubId, initialMembers }: MembersListProps) {
// useOptimistic hook
const [optimisticMembers, removeOptimisticMember] = useOptimistic(
initialMembers,
(state, removedId: string) => state.filter(m => m.id !== removedId)
);
const handleRemove = async (memberId: string) => {
// 1. Update UI instantly (optimistic)
removeOptimisticMember(memberId);
// 2. Call server action
const result = await removeMemberAction(clubId, memberId);
if (result.success) {
// Success: revalidatePath() in action will trigger Server Component re-fetch
// → initialMembers updated → optimisticMembers synced automatically
toast.success('Membre retiré avec succès');
} else {
// Error: revalidatePath() still called in action
// → Server Component re-fetches → initialMembers restored → ROLLBACK AUTO
toast.error(result.error.message);
}
};
return (
<ul className="space-y-2">
{optimisticMembers.length === 0 ? (
<p className="text-muted-foreground">Aucun membre</p>
) : (
optimisticMembers.map(member => (
<MemberCard
key={member.id}
member={member}
onRemove={() => handleRemove(member.id)}
/>
))
)}
</ul>
);
}
// ==========================================
// 3. Server Action avec revalidatePath
// ==========================================
// actions/remove-member.action.ts
'use server';
import { revalidatePath } from 'next/cache';
import { membersApi } from '../api/members.api';
export async function removeMemberAction(clubId: string, memberId: string) {
try {
// Call backend API
await membersApi.remove(clubId, memberId);
// CRITICAL: Revalidate to trigger Server Component re-fetch
// This works for BOTH success AND error cases
revalidatePath(`/clubs/${clubId}/members`);
return { success: true };
} catch (error) {
// Even on error, revalidate to restore correct state
revalidatePath(`/clubs/${clubId}/members`);
return {
success: false,
error: {
code: 'REMOVE_ERROR',
message: 'Impossible de retirer le membre',
},
};
}
}
Pourquoi ça fonctionne :
- Optimistic update → UI se met à jour immédiatement
- Server Action exécute → Succès OU erreur
revalidatePath()appelé dans TOUS les cas → Server Component re-fetchinitialMembersmis à jour →optimisticMemberssynchronisé automatiquement- Si erreur : Le re-fetch restaure l'état correct = Rollback automatique
🔄 Alternative : Rollback Manuel avec State
Si vous ne pouvez pas utiliser Server Components :
'use client';
import { useState } from 'react';
import { removeMemberAction } from '../actions/remove-member.action';
export function MembersList({ initialMembers }: Props) {
const [members, setMembers] = useState(initialMembers);
const [pendingRemoveIds, setPendingRemoveIds] = useState<string[]>([]);
// Filter out pending removals (optimistic)
const displayedMembers = members.filter(m => !pendingRemoveIds.includes(m.id));
const handleRemove = async (memberId: string) => {
// 1. Add to pending (optimistic update)
setPendingRemoveIds(prev => [...prev, memberId]);
// 2. Call server action
const result = await removeMemberAction(memberId);
if (result.success) {
// Success: Remove from actual state
setMembers(prev => prev.filter(m => m.id !== memberId));
setPendingRemoveIds(prev => prev.filter(id => id !== memberId));
toast.success('Membre retiré');
} else {
// Error: ROLLBACK manually
setPendingRemoveIds(prev => prev.filter(id => id !== memberId));
toast.error(result.error.message);
}
};
return (
<ul>
{displayedMembers.map(member => (
<MemberCard key={member.id} member={member} onRemove={() => handleRemove(member.id)} />
))}
</ul>
);
}
🎨 Autres Patterns
Add Operation
// Server Component + useOptimistic (Recommended)
export function ItemsList({ initialItems }: Props) {
const [optimisticItems, addOptimisticItem] = useOptimistic(
initialItems,
(state, newItem: Item) => [...state, newItem]
);
const handleAdd = async (item: Item) => {
// Optimistic
addOptimisticItem(item);
// Server action (with revalidatePath)
const result = await addItemAction(item);
if (result.success) {
toast.success('Ajouté');
// revalidatePath() in action → Server Component re-fetch → Sync auto
} else {
toast.error(result.error.message);
// revalidatePath() in action → Server Component re-fetch → Rollback auto
}
};
return <div>...</div>;
}
Update Operation
export function ItemsList({ initialItems }: Props) {
const [optimisticItems, updateOptimisticItem] = useOptimistic(
initialItems,
(state, updated: Item) => state.map(i => i.id === updated.id ? updated : i)
);
const handleUpdate = async (item: Item) => {
// Optimistic
updateOptimisticItem(item);
// Server action (with revalidatePath)
const result = await updateItemAction(item);
if (result.success) {
toast.success('Modifié');
} else {
toast.error(result.error.message);
}
};
return <div>...</div>;
}
Toggle Operation
export function ToggleComponent({ initialItem }: Props) {
const [optimisticItem, toggleOptimistic] = useOptimistic(
initialItem,
(state) => ({ ...state, active: !state.active })
);
const handleToggle = async () => {
// Optimistic
toggleOptimistic();
// Server action (with revalidatePath)
const result = await toggleItemAction(optimisticItem.id);
if (!result.success) {
toast.error(result.error.message);
// revalidatePath() in action → Rollback auto
}
};
return (
<button onClick={handleToggle}>
{optimisticItem.active ? 'Active' : 'Inactive'}
</button>
);
}
🔍 Exemple Complet avec Multiple Operations
// components/MembersList.tsx
'use client';
import { useOptimistic } from 'react';
import { removeMemberAction, updateMemberRoleAction } from '../actions';
export function MembersList({ clubId, initialMembers }: Props) {
const [optimisticMembers, updateOptimisticMembers] = useOptimistic(
initialMembers,
(state, action: { type: 'remove' | 'updateRole'; id: string; role?: string }) => {
switch (action.type) {
case 'remove':
return state.filter(m => m.id !== action.id);
case 'updateRole':
return state.map(m => m.id === action.id ? { ...m, role: action.role! } : m);
default:
return state;
}
}
);
const handleRemove = async (memberId: string) => {
updateOptimisticMembers({ type: 'remove', id: memberId });
const result = await removeMemberAction(clubId, memberId);
if (result.success) {
toast.success('Membre retiré');
} else {
toast.error(result.error.message);
}
// revalidatePath() in action handles sync/rollback
};
const handleUpdateRole = async (memberId: string, newRole: string) => {
updateOptimisticMembers({ type: 'updateRole', id: memberId, role: newRole });
const result = await updateMemberRoleAction(clubId, memberId, newRole);
if (result.success) {
toast.success('Rôle modifié');
} else {
toast.error(result.error.message);
}
// revalidatePath() in action handles sync/rollback
};
return (
<ul>
{optimisticMembers.map(member => (
<MemberCard
key={member.id}
member={member}
onRemove={() => handleRemove(member.id)}
onUpdateRole={(role) => handleUpdateRole(member.id, role)}
/>
))}
</ul>
);
}
✅ Checklist useOptimistic
-
useOptimisticpour operations importantes (delete, update, toggle) - Server Component fournit
initialData(Recommended) - Optimistic update AVANT server action call
- Server Action appelle
revalidatePath()dans TOUS les cas (succès ET erreur) - Toast pour feedback utilisateur (succès/erreur)
- Pas de rollback manuel si Server Component + revalidatePath
- Si pas Server Component : Rollback manuel avec state
🚨 Erreurs Courantes
1. Oublier revalidatePath
// ❌ MAUVAIS - Pas de revalidatePath = Pas de sync/rollback
export async function removeMemberAction(id: string) {
await api.remove(id);
return { success: true };
// UI reste dans l'état optimiste, même si erreur backend !
}
// ✅ BON - revalidatePath dans TOUS les cas
export async function removeMemberAction(id: string) {
try {
await api.remove(id);
revalidatePath('/members'); // Success: sync
return { success: true };
} catch (error) {
revalidatePath('/members'); // Error: rollback
return { success: false, error: { message: 'Erreur' } };
}
}
2. revalidatePath uniquement si succès
// ❌ MAUVAIS - Rollback ne se fait pas
export async function removeMemberAction(id: string) {
try {
await api.remove(id);
revalidatePath('/members'); // Only on success
return { success: true };
} catch (error) {
// No revalidatePath = No rollback !
return { success: false, error };
}
}
// ✅ BON - revalidatePath dans les DEUX cas
export async function removeMemberAction(id: string) {
try {
await api.remove(id);
revalidatePath('/members');
return { success: true };
} catch (error) {
revalidatePath('/members'); // CRITICAL for rollback
return { success: false, error };
}
}
3. Utiliser useOptimistic sans Server Component
// ❌ MAUVAIS - useOptimistic avec state local = compliqué
const [members, setMembers] = useState(initialMembers);
const [optimistic, setOptimistic] = useOptimistic(members, ...);
// Rollback manuel requis, complexe à gérer
// ✅ BON - useOptimistic avec Server Component
export default async function Page() {
const members = await fetchMembers(); // Server-side
return <MembersList initialMembers={members} />;
}
function MembersList({ initialMembers }) {
const [optimistic, update] = useOptimistic(initialMembers, ...);
// Rollback automatique via revalidatePath
}
📚 Skills Complémentaires
- server-actions : Server Actions avec revalidatePath
- suspense-streaming : Loading states pendant operations
- atomic-component : Composants utilisant useOptimistic
Rappel CRITIQUE : useOptimistic ne rollback PAS automatiquement. Utilisez Server Component + revalidatePath pour rollback automatique, ou gérez manuellement avec state.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon