import { useOptimistic, useTransition, useRef } from 'react';
type Comment = { id: string; text: string; sending?: boolean };
export function CommentSection({ comments, addCommentAction }: { comments: Comment[]; addCommentAction: (text: string) => Promise<void> }) {
const formRef = useRef<HTMLFormElement>(null);
const [isPending, startTransition] = useTransition();
const [optimisticComments, setOptimisticComment] = useOptimistic(
comments,
(state, newText: string) => [
...state,
{ id: crypto.randomUUID(), text: newText, sending: true }
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('comment') as string;
if (!text.trim()) return;
formRef.current?.reset();
startTransition(async () => {
setOptimisticComment(text);
try {
await addCommentAction(text);
} catch (err) {
// React resetează automat starea la 'comments' la finalul tranziției în caz de eroare
alert('Nu am putut salva comentariul.');
}
});
}
return (
<div>
<ul>
{optimisticComments.map((c) => (
<li key={c.id} style={{ opacity: c.sending ? 0.6 : 1 }}>
{c.text} {c.sending && <small>(se trimite...)</small>}
</li>
))}
</ul>
<form ref={formRef} action={handleSubmit}>
<input name="comment" required disabled={isPending} />
<button type="submit" disabled={isPending}>Trimite</button>
</form>
</div>
);
}Am trecut recent un modul de feedback pe React 19 la un client cu vreo 12k de review-uri lunare. Problema lor clasică: userul scria un comentariu, apăsa „Trimite” și aștepta 450-600ms până când backend-ul procesa request-ul și întorcea răspunsul, timp în care interfața bloca formularul cu un spinner obositor.
Cu useOptimistic, am scăzut latența percepută la exact 0ms. Userul vede comentariul instant în listă, iar dacă serverul dă chix, starea revine automat la ce era înainte, fără să scriu eu manual cod de curățare.
Cum gândești o stare optimistă fără să te încurci
Conceptul nu e nou, făceam asta și în Redux sau React Query, dar trebuia să scrii manual logica de onMutate, onError și restaurare de state. Era un boilerplate lung și de multe ori uitam de edge case-uri.
Hook-ul useOptimistic din React 19 primește starea reală (sursa de adevăr venită din props sau dintr-un Server Component) și o funcție de update. În momentul în care declanșezi acțiunea, apelezi dispatcher-ul optimist cu valoarea provizorie.
Cât timp Server Action-ul sau request-ul e în desfășurare, React va reda UI-ul folosind starea optimistă. Când request-ul se termină (fie cu succes, fie cu eroare), React aruncă starea temporară și trece înapoi pe starea reală venită din revalidare.
Un trade-off real de care m-am lovit
Sună excelent în teorie, dar în practică există un dezavantaj clar: ID-urile temporare și dependența de backend.
Dacă comentariul tău are nevoie de un ID generat de baza de date (de exemplu un UUID sau BigInt din Postgres), pe client va trebui să-i generezi un ID temporar cu crypto.randomUUID(). Dacă userul vrea să dea „Șterge” pe comentariul optimist înainte ca serverul să răspundă, n-ai ID-ul real. Asta înseamnă că trebuie să blochezi acțiunile secundare pe itemele provizorii (de exemplu, arăți un indicator discret de „se trimite...” și dezactivezi butonul de ștergere pe acel item).
În plus, dacă ai un user pe o rețea 3G groaznică care trimite 3 comentarii rapid, trebuie să fii atent la ordinea în care se execută acțiunile, altfel UI-ul optimist se poate desincroniza temporar cu ce e pe server.
Concluzia mea
Pentru formulare simple, like-uri, comentarii sau toggle-uri de bookmark, useOptimistic e genial și scurtează codul cu 50%. Pentru workflow-uri complexe cu ecrane de checkout sau tranzacții financiare, unde nu-ți permiți să minți userul nici 100ms că operațiunea a reușit, mai bine rămâi pe loader-ul clasic.
Ați început să folosiți feat-urile noi din React 19 în producție sau încă așteptați să se mai așeze ecosistemul de librării?