import { useOptimistic, useState } from "react";
type Comment = { id: string; text: string; sending?: boolean };
export function CommentSection({ initialComments }: { initialComments: Comment[] }) {
const [comments, setComments] = useState<Comment[]>(initialComments);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newCommentText: string) => [
...state,
{ id: Math.random().toString(), text: newCommentText, sending: true }
]
);
async function handleFormSubmit(formData: FormData) {
const text = formData.get("comment") as string;
if (!text) return;
addOptimisticComment(text);
try {
const savedComment = await saveCommentToDB(text); // API Call
setComments((prev) => [...prev, savedComment]);
} catch (err) {
alert("N-am putut salva comentariul. Rollback automat!");
}
}
return (
<div>
{optimisticComments.map((c) => (
<p key={c.id} style={{ opacity: c.sending ? 0.5 : 1 }}>
{c.text} {c.sending && "(se trimite...)"}
</p>
))}
<form action={handleFormSubmit}>
<input name="comment" placeholder="Scrie un comentariu..." required />
<button type="submit">Trimite</button>
</form>
</div>
);
}Salutare! Am trecut recent un modul de comentarii pe React 19 și am vrut să scap de clasicele stări de loading care fac aplicația să se simtă leneșă. Am folosit useOptimistic și utilizatorii văd acum comentariul instant, fără să mai aștepte cele 800ms de latență ale API-ului.
Dacă vrei să îmbunătățești UX-ul fără să te complici cu Redux sau stări globale masive, hook-ul ăsta e exact ce trebuie.
Problema cu loading state-urile clasice
Înainte, când un user dădea "Trimite", puneam un isPending pe true, arătam un spinner obosit, așteptam răspunsul de la server și abia apoi actualizam lista. E o experiență destul de proastă. Cu update-urile optimiste, adăugăm direct elementul în UI, presupunând că baza de date o să-l accepte. Dacă crapă API-ul, facem rollback.
Am testat abordarea asta pe un proiect cu peste 12.000 de utilizatori activi lunar. Rata de eroare la salvarea comentariilor este de sub 0.3%, deci n-are absolut niciun sens să pedepsim 99.7% din useri cu un spinner doar pentru cazurile rare de eroare de rețea.
Cum funcționează useOptimistic?
Hook-ul primește starea reală (sursa de adevăr) și o funcție de tip reducer care decide cum se îmbină starea optimistă temporară.
Partea cu adevărat mișto? React se prinde singur când se termină acțiunea asincronă (care rulează într-un Transition sau direct într-un Form Action) și revine automat la starea reală trimisă de server. Nu mai trebuie să scrii tu manual logică de curățare pe finally.
Dar vine și cu un trade-off sincer. Dacă utilizatorul trimite trei comentarii la rând foarte rapid, ordinea lor în UI-ul optimist s-ar putea să difere de ID-urile finale generate de baza de date. Asta poate duce la un mic "flicker" vizual în momentul în care se sincronizează starea reală de pe server.
Exemplu concret de implementare
În codul de mai jos, folosim useOptimistic legat de o acțiune de formular. Cât timp handleFormSubmit rulează (fiind o acțiune asincronă), UI-ul afișează comentariul cu o opacitate redusă (semn că se trimite). Dacă API-ul aruncă o eroare, React face rollback automat la lista anterioară, iar noi doar prindem eroarea ca să punem o alertă.
Limitările de care m-am lovit
Nu totul e perfect în React 19. useOptimistic este strâns legat de conceptul de Actions și Transitions. Dacă încerci să-l apelezi în afara unui context de tranziție, pur și simplu nu va funcționa și îți va arunca erori în consolă.
De asemenea, reconcilierea poate fi ciudată dacă serverul modifică datele trimise (de exemplu, dacă sanitizează textul sau adaugă automat link-uri). Utilizatorul va vedea textul lui brut pentru o secundă, după care textul se va schimba brusc în varianta procesată de server. Pentru comentarii simple e ok, dar la editoare complexe de text devine deranjant.
Voi cum gestionați stările astea? Mergeți pe varianta clasică de spinnere, sau folosiți deja pattern-uri optimiste în producție?