Skip to content
Back to skills

Collection

ASecurity

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".

  • 24 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 8, 2026
developmenttypescriptreactapibackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 21 files and shows the line behind each finding

Scanned September 8, 2026

npx -y skills add mattnigh/skills_collection --skill collection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Collection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Collection
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mattnigh-collection-5f8fcaeb/badge)](https://www.skillsdirectory.com/skills/mattnigh-collection-5f8fcaeb)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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** :
```typescript
// ❌ 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

```typescript
// ==========================================
// 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** :
1. Optimistic update → UI se met à jour immédiatement
2. Server Action exécute → Succès OU erreur
3. `revalidatePath()` appelé dans TOUS les cas → Server Component re-fetch
4. `initialMembers` mis à jour → `optimisticMembers` synchronisé automatiquement
5. **Si erreur** : Le re-fetch restaure l'état correct = **Rollback automatique**

## 🔄 Alternative : Rollback Manuel avec State

Si vous **ne pouvez pas** utiliser Server Components :

```typescript
'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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
// 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

- [ ] `useOptimistic` pour 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

```typescript
// ❌ 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

```typescript
// ❌ 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

```typescript
// ❌ 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.

Files in this skill

  • 0Chan-smc__claude-code-workflow-lab__claude__skills__frontend-dev-guidelines__SKILL.md15.1 KB
  • 17hz__nextjs-template__claude__skills__example-skill__SKILL.md316 B
  • 1ambda__dataops-platform__claude__skills__context-synthesis__SKILL.md3.5 KB
  • 1natsu172__dotfiles__claude__skills__git-analysis__SKILL.md5.4 KB
  • 1natsu172__dotfiles__claude__skills__github-pr-best-practices__SKILL.md7.7 KB
  • 23Maestro__prospect-pipeline__claude__skills__npid-fastapi-skill.md26.1 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-code-javascript__SKILL.md15.7 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-code-python__SKILL.md17.5 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-expression-syntax__SKILL.md9.4 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-mcp-tools-expert__SKILL.md12.5 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-node-configuration__SKILL.md16.6 KB
  • 360AYA25__ClaudeN8N__claude__skills__n8n-workflow-patterns__SKILL.md11.2 KB
  • 3x-Projetos__claude-memory-framework__claude__skills__scientist__SKILL.md14.8 KB
  • 5MinFutures__futures-arena__claude__skills__migration-tracker__SKILL.md16.2 KB
  • 5MinFutures__futures-arena__claude__skills__planning-guidelines__SKILL.md11.8 KB
  • 92Bilal26__TaskPilotAI__claude__skills__assessment-builder__SKILL.md17.5 KB
  • 92Bilal26__TaskPilotAI__claude__skills__book-scaffolding__SKILL.md19.1 KB
  • 92Bilal26__TaskPilotAI__claude__skills__code-validation-sandbox__SKILL.md6.2 KB
  • 92Bilal26__TaskPilotAI__claude__skills__exercise-designer__SKILL.md18.1 KB
  • 92Bilal26__TaskPilotAI__claude__skills__learning-objectives__SKILL.md24.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…