useOptimistic è un hook di React 19 che permette allo schermo di mostrare il risultato di un'azione prima che l'azione termini. Imposti un valore ottimistico dentro un'azione; React lo mostra mentre l'azione è in attesa, poi torna allo stato reale. Se la richiesta è fallita, lo stato reale non è mai cambiato, quindi lo schermo si ripristina da solo.
La finta API di questo esempio fa le veci di una richiesta reale e fallisce a ogni terza chiamata, così puoi vedere entrambi gli esiti.
Clicca il pulsante tre volte, aspettando un secondo tra un clic e l'altro. I primi due cambiano subito e la riga del salvataggio si allinea un secondo dopo. Anche il terzo cambia subito, poi torna indietro quando la richiesta fallisce. Cambia calls % 3 in calls % 2 e fallisce un clic ogni due.
La sintassi
const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
stateè il valore reale, di solito dauseState, dalle props o dauseActionState. Quando nessuna azione è in attesa,optimisticStateè esattamente questo valore.setOptimistic(value)imposta il valore ottimistico per tutto il tempo in cui l'azione attuale è in esecuzione.updateFnè facoltativa:(currentState, optimisticValue) => nextState. Con essa,setOptimisticriceve una modifica invece di un valore completamente nuovo. Vedi la forma con reducer qui sotto.
Il ciclo di vita di un clic è sempre lo stesso:
- Dentro un'azione, chiami
setOptimistic. Lo schermo si aggiorna subito. - L'azione attende la richiesta.
- In caso di successo aggiorni lo stato reale (dentro
startTransitionquando avviene dopo unawait). - L'azione termina.
optimisticStatetorna a seguirestate: il valore salvato in caso di successo, il vecchio valore in caso di errore.
Il passaggio 4 è il motivo per cui nell'esempio non c'è codice di ripristino. Il blocco catch imposta solo un messaggio di errore.
Deve essere eseguito dentro un'azione
Il valore ottimistico esiste solo mentre una transizione è in attesa, quindi setOptimistic deve essere chiamato dentro una transizione. Valgono tutti questi casi:
- la funzione che passi a
startTransition(o allostartTransitiondiuseTransition), - una funzione passata a
<form action={...}>o a<button formAction={...}>, - l'azione che dai a useActionState.
Chiamato da un semplice onClick, il valore ottimistico non ha un'azione in cui vivere. React lo annulla subito, e in sviluppo registra "An optimistic state update occurred outside a transition or action".
Gli aggiornamenti dello stato reale che avvengono dopo un await vanno racchiusi nel loro startTransition, come startTransition(() => setLiked(next)) qui sopra. Dopo un await, React non riesce a capire che sei ancora dentro la transizione precedente.
Una lista di messaggi con la forma con reducer
Quando lo stato ottimistico è una lista, passa una funzione di aggiornamento come secondo argomento. Il setter riceve allora il nuovo elemento, e React lo aggiunge in fondo a qualunque sia la lista reale in quel momento.
Invia tre messaggi con testi diversi, uno alla volta. Ognuno compare attenuato con "(sending...)" e diventa pieno una volta consegnato; il terzo sparisce dalla chat e compare come "Not sent". Inviali invece rapidamente e tutti e tre restano attenuati finché l'ultima richiesta non termina, perché React mantiene i valori ottimistici finché ogni azione in attesa non è terminata. La form action è già una transizione, quindi qui addOptimistic non ha bisogno di un suo startTransition.
Dai a ogni elemento ottimistico una key che non entri in conflitto con gli elementi reali. L'esempio usa pending- più il testo, quindi inviare lo stesso testo due volte mentre entrambi sono in attesa creerebbe un conflitto; un'app reale creerebbe un id sul client.
Più aggiornamenti contemporaneamente
Poiché la funzione di aggiornamento riceve lo stato attuale, gli aggiornamenti ottimistici si sommano. Clicca rapidamente e ogni clic in attesa viene applicato sopra l'ultimo.
Clicca quattro volte velocemente. Il carrello mostra subito 4. Quando le richieste terminano, il conteggio si stabilizza su 3, perché la terza richiesta è fallita e la console lo dice. React mantiene i valori ottimistici finché ogni azione in attesa non è terminata, poi mostra il conteggio reale.
Quando usarlo, e quando no
Gli aggiornamenti ottimistici vanno bene per azioni che riescono quasi sempre e sono facili da annullare: mi piace, stelle, interruttori, rinominare, aggiungere un messaggio. Nel caso comune l'utente non vede alcuno spinner.
Evitali dove un falso "fatto" sarebbe fuorviante: pagamenti, eliminazione di un account, qualsiasi cosa su cui l'utente potrebbe agire prima di conoscere il risultato. In questi casi mostra uno stato di attesa con isPending di useActionState o di useTransition e aspetta la risposta reale.
Avvisa sempre l'utente quando avviene un ripristino. Un valore che torna indietro in silenzio sembra un bug. Entrambi gli esempi qui sopra tengono un messaggio di errore in uno stato normale così sopravvive all'azione.
Con useActionState
useOptimistic e useActionState vanno d'accordo. L'azione di useActionState è già una transizione, quindi puoi impostare il valore ottimistico all'inizio, e lo stato restituito è il valore reale a cui torna quello ottimistico.
const [state, formAction] = useActionState(async (previous, formData) => {
const title = formData.get('title');
setOptimisticTitle(title);
const saved = await saveTitle(title);
return { title: saved.title };
}, { title: 'Untitled' });
const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);
Mentre il salvataggio è in corso, la pagina mostra il nuovo titolo. Quando si risolve, state.title contiene il valore salvato. Se saveTitle genera un errore, l'errore dell'azione va all'error boundary più vicino; per mostrare invece il vecchio titolo con un messaggio, intercetta l'errore dentro l'azione e restituisci lo stato precedente più un errore.
Errori comuni
Chiamare il setter fuori da un'azione. Il valore ottimistico non resta sullo schermo, e React avvisa in sviluppo. Racchiudilo in startTransition o spostalo in una form action.
Dimenticare di aggiornare lo stato reale. In caso di successo, l'azione deve cambiare lo stato che useOptimistic rispecchia (setLiked, setMessages). Altrimenti il valore ottimistico sparisce quando l'azione termina, e una richiesta riuscita sembra un ripristino.
Aggiornare lo stato reale dopo un await senza una transizione. L'aggiornamento arriva comunque, ma React potrebbe mostrarlo in un momento diverso dalla fine dell'azione. Racchiudilo in startTransition, come fanno tutti e tre gli esempi.
Usare il valore ottimistico come fonte di verità. Invia le richieste e calcola i totali a partire dallo stato reale. Il valore ottimistico serve solo per la visualizzazione e può essere buttato via in qualsiasi momento.
Domande frequenti
Cosa fa useOptimistic?
Ti dà una copia di un pezzo di stato che puoi cambiare immediatamente mentre è in esecuzione un'azione asincrona. Quando l'azione termina, la copia torna a seguire lo stato reale, che a quel punto contiene il risultato salvato oppure il vecchio valore.
Come fa useOptimistic a ripristinare in caso di errore?
Non ha bisogno di un percorso speciale per gli errori. Il valore ottimistico vive solo mentre l'azione è in attesa. Se la richiesta fallisce e non aggiorni mai lo stato reale, l'hook mostra di nuovo lo stato reale, cioè il valore di prima del clic.
Perché ricevo "An optimistic state update occurred outside a transition or action"?
Il setter è stato chiamato da un semplice gestore di eventi. Chiamalo dentro una funzione passata a startTransition, dentro un <form action> o dentro un'azione di useActionState.
Qual è il secondo argomento di useOptimistic?
Una funzione di aggiornamento facoltativa, (currentState, optimisticValue) => newState, simile a un reducer. Con essa il setter riceve solo la modifica (un nuovo messaggio, +1) e React calcola lo stato ottimistico, anche quando più aggiornamenti sono in attesa contemporaneamente.
useOptimistic serve solo per i form?
No. Funziona in qualsiasi transizione, quindi un pulsante che chiama startTransition(async () => { ... }) può usarlo tanto quanto una form action.