useOptimistic to hook z React 19, który pozwala ekranowi pokazać wynik akcji, zanim akcja się skończy. Ustawiasz optymistyczną wartość wewnątrz akcji; React pokazuje ją, dopóki akcja oczekuje, a potem wraca do prawdziwego stanu. Jeśli żądanie się nie powiodło, prawdziwy stan nigdy się nie zmienił, więc ekran sam się cofa.
Udawane API w tym przykładzie zastępuje prawdziwe żądanie i zawodzi przy co trzecim wywołaniu, żebyś mógł zobaczyć oba wyniki.
Kliknij przycisk trzy razy, czekając sekundę między kliknięciami. Dwa pierwsze przełączają się od razu, a linia z zapisem dogania je sekundę później. Trzecie też przełącza się od razu, a potem wraca, gdy żądanie się nie powiedzie. Zmień calls % 3 na calls % 2, a zawiedzie co drugie kliknięcie.
Składnia
const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
stateto prawdziwa wartość, zwykle zuseState, propsów albouseActionState. Gdy żadna akcja nie oczekuje,optimisticStateto dokładnie ta wartość.setOptimistic(value)ustawia optymistyczną wartość na czas trwania bieżącej akcji.updateFnjest opcjonalne:(currentState, optimisticValue) => nextState. Z nimsetOptimisticdostaje zmianę, a nie całą nową wartość. Zobacz formę z reduktorem niżej.
Cykl życia jednego kliknięcia jest zawsze taki sam:
- Wewnątrz akcji wywołujesz
setOptimistic. Ekran od razu się aktualizuje. - Akcja czeka na żądanie.
- Przy sukcesie aktualizujesz prawdziwy stan (wewnątrz
startTransition, gdy następuje to poawait). - Akcja się kończy.
optimisticStateznowu podąża zastate: przy sukcesie za zapisaną wartością, przy niepowodzeniu za starą.
Krok 4 to powód, dla którego w przykładzie nie ma kodu wycofującego zmianę. Blok catch ustawia tylko komunikat o błędzie.
Musi działać wewnątrz akcji
Optymistyczna wartość istnieje tylko wtedy, gdy przejście oczekuje, więc setOptimistic trzeba wywołać wewnątrz niego. Wszystkie te miejsca się liczą:
- funkcja, którą przekazujesz do
startTransition(albo dostartTransitionzuseTransition), - funkcja przekazana do
<form action={...}>albo<button formAction={...}>, - akcja, którą przekazujesz do useActionState.
Wywołana ze zwykłego onClick optymistyczna wartość nie ma akcji, w której mogłaby żyć. React od razu ją cofa, a w trybie deweloperskim wypisuje "An optimistic state update occurred outside a transition or action".
Aktualizacje prawdziwego stanu, które następują po await, powinny być owinięte we własne startTransition, tak jak startTransition(() => setLiked(next)) powyżej. Po await React nie potrafi stwierdzić, że wciąż jesteś wewnątrz wcześniejszego przejścia.
Lista wiadomości w formie z reduktorem
Gdy optymistyczny stan jest listą, przekaż funkcję aktualizującą jako drugi argument. Setter dostaje wtedy nowy element, a React dopisuje go do takiej prawdziwej listy, jaka jest w danej chwili.
Wyślij trzy wiadomości z różnym tekstem, po jednej. Każda pojawia się wyblakła z "(sending...)" i staje się pełna po dostarczeniu; trzecia znika z czatu i pokazuje się jako "Not sent". Wyślij je zamiast tego szybko, a wszystkie trzy zostaną wyblakłe, dopóki nie zakończy się ostatnie żądanie, bo React trzyma optymistyczne wartości, dopóki każda oczekująca akcja się nie skończy. Akcja formularza jest już przejściem, więc addOptimistic nie potrzebuje tu własnego startTransition.
Każdemu optymistycznemu elementowi nadaj klucz, który nie zderzy się z prawdziwymi elementami. Przykład używa pending- i tekstu, więc dwukrotne wysłanie tego samego tekstu, gdy oba oczekują, dałoby kolizję; prawdziwa aplikacja utworzyłaby id po stronie klienta.
Kilka aktualizacji naraz
Ponieważ funkcja aktualizująca dostaje bieżący stan, optymistyczne aktualizacje się nakładają. Klikaj szybko, a każde oczekujące kliknięcie zostanie zastosowane na poprzednim.
Kliknij szybko cztery razy. Koszyk od razu pokazuje 4. Gdy żądania się zakończą, licznik ustali się na 3, bo trzecie żądanie się nie powiodło i konsola to mówi. React trzyma optymistyczne wartości, dopóki każda oczekująca akcja się nie skończy, a potem pokazuje prawdziwy licznik.
Kiedy go używać, a kiedy nie
Optymistyczne aktualizacje pasują do akcji, które prawie zawsze się udają i łatwo je cofnąć: polubienia, gwiazdki, przełączniki, zmiana nazwy, dodanie wiadomości. W typowym przypadku użytkownik nie widzi żadnego spinnera.
Unikaj ich tam, gdzie fałszywe „gotowe" wprowadziłoby w błąd: płatności, usunięcie konta, wszystko, na podstawie czego użytkownik może działać, zanim wynik będzie znany. W takich przypadkach pokaż stan oczekiwania przez isPending z useActionState albo useTransition i poczekaj na prawdziwą odpowiedź.
Zawsze informuj użytkownika, gdy następuje wycofanie. Wartość, która po cichu wraca, wygląda jak błąd. Oba przykłady powyżej trzymają komunikat o błędzie w zwykłym stanie, żeby przetrwał akcję.
Z useActionState
useOptimistic i useActionState pasują do siebie. Akcja z useActionState jest już przejściem, więc możesz ustawić optymistyczną wartość na jej początku, a zwrócony stan to prawdziwa wartość, do której wraca optymistyczna.
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);
Gdy trwa zapis, strona pokazuje nowy tytuł. Gdy się rozwiąże, state.title zawiera zapisaną wartość. Jeśli saveTitle rzuci błąd, błąd akcji trafi do najbliższej granicy błędów; żeby zamiast tego pokazać stary tytuł z komunikatem, złap błąd wewnątrz akcji i zwróć poprzedni stan razem z błędem.
Częste błędy
Wywoływanie settera poza akcją. Optymistyczna wartość nie zostaje na ekranie, a React ostrzega w trybie deweloperskim. Owiń ją w startTransition albo przenieś do akcji formularza.
Zapomnienie o aktualizacji prawdziwego stanu. Przy sukcesie akcja musi zmienić stan, który odzwierciedla useOptimistic (setLiked, setMessages). Inaczej optymistyczna wartość zniknie, gdy akcja się skończy, a udane żądanie będzie wyglądać jak wycofanie.
Aktualizacja prawdziwego stanu po await bez przejścia. Aktualizacja i tak trafi na ekran, ale React może pokazać ją w innym momencie niż koniec akcji. Owiń ją w startTransition, jak robią to wszystkie trzy przykłady.
Traktowanie optymistycznej wartości jako źródła prawdy. Wysyłaj żądania i licz sumy z prawdziwego stanu. Optymistyczna wartość służy tylko do wyświetlania i w każdej chwili może zostać wyrzucona.
Najczęściej zadawane pytania
Co robi useOptimistic?
Daje ci kopię kawałka stanu, którą możesz od razu zmienić, dopóki działa asynchroniczna akcja. Gdy akcja się kończy, kopia znowu podąża za prawdziwym stanem, który do tego czasu zawiera albo zapisany wynik, albo starą wartość.
Jak useOptimistic wycofuje zmianę przy błędzie?
Nie potrzebuje specjalnej ścieżki dla błędu. Optymistyczna wartość żyje tylko wtedy, gdy akcja oczekuje. Jeśli żądanie się nie powiedzie, a ty nigdy nie zaktualizujesz prawdziwego stanu, hook znowu pokaże prawdziwy stan, czyli wartość sprzed kliknięcia.
Dlaczego dostaję "An optimistic state update occurred outside a transition or action"?
Setter został wywołany ze zwykłego handlera zdarzenia. Wywołaj go wewnątrz funkcji przekazanej do startTransition, wewnątrz <form action> albo wewnątrz akcji z useActionState.
Jaki jest drugi argument useOptimistic?
Opcjonalna funkcja aktualizująca, (currentState, optimisticValue) => newState, podobna do reduktora. Z nią setter dostaje tylko zmianę (nową wiadomość, +1), a React wylicza optymistyczny stan, nawet gdy naraz oczekuje kilka aktualizacji.
Czy useOptimistic służy tylko do formularzy?
Nie. Działa w każdym przejściu, więc przycisk, który wywołuje startTransition(async () => { ... }), może go używać tak samo jak akcja formularza.