Formularze asynchroniczne w React 19 z użyciem use, useActionState i useOptimistic
Jak use(), useActionState, useFormStatus i useOptimistic zastępują ręczne ładowanie oraz flagi błędów w React 19, wraz z pułapkami, które każdy z tych hooków po cichu ukrywa.
Przed React 19 niemal każde żądanie lub formularz wymagały tego samego fundamentu: useState do przechowywania danych, useEffect do ich ładowania oraz flag do wskazywania stanu ładowania, błędów i wyników. React 19 wprowadza cztery nowe API, które przenoszą te funkcje do samego frameworka: use(), useActionState, useFormStatus i useOptimistic. Poniżej wyjaśniono, co robi każde z nich, jak ze sobą współpracują oraz jakie ograniczenia łatwo przeoczyć. Aby uzyskać szerszy przegląd aktualizacji, zapoznaj się z triagą aktualizacji React 19.
Odczytywanie obietnic podczas renderowania za pomocą use()
use() nie jest hakiem w tradycyjnym rozumieniu. W odróżnieniu od haków może być wywoływany wewnątrz warunków, pętli lub po przedwczesnym powrocie. Podczas renderowania odczytuje wartość Promise lub Context i współpracuje z Suspense, aby pokazać stan oczekiwania.
import { use, Suspense } from "react";
UserCard otrzymuje promise i rozbija go za pomocą use; dopóki nie zostanie rozwiązany, najbliższa granica Suspense w Page renderuje jego alternatywę.
function UserCard({ userPromise }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}function Page({ userPromise }) {
return (
<Suspense fallback={<p>Loading user…</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}
Komponent nadrzędny tworzy promise, komponent potomny go konsumuje, a Suspense odpowiada za wyświetlanie stanu ładowania. Istnieje jedno ważne ograniczenie: promise musi być stabilny. Promise utworzony podczas renderowania komponentu klienckiego jest nowy przy każdym renderowaniu i powoduje wielokrotne wstrzymanie wykonywania, dlatego należy go tworzyć w komponencie serwerowym, w załadowarce z pamięcią cache lub w API danych frameworka.
Zarządzanie wysyłkami za pomocą useActionState
useActionState zastępuje ręcznie zarządzane stany isSubmitting, error i result. Przekazujesz akcję oraz stan początkowy; otrzymujesz aktualny stan, zapakowaną akcję dla formularza oraz flagę wskazującą na oczekiwanie. Akcja otrzymuje poprzedni stan oraz FormData, a to, co zwróci, staje się następnym stanem.
import { useActionState } from "react";
Tutaj submitFeedback weryfikuje wiadomość, zapisuje ją i zwraca albo komunikat o błędzie, albo sygnał sukcesu, który formularz wyświetla bezpośrednio.
async function submitFeedback(prevState, formData) {
const message = formData.get("message");
if (message.length < 5) {
return { error: "Message is too short." };
}
await saveFeedback(message);
return { success: true };
}function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {}); return (
<form action={formAction}>
<textarea name="message" required />
<button disabled={isPending}>
{isPending ? "Sending…" : "Send feedback"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p>Thanks for the feedback!</p>}
</form>
);
}
Ponieważ operacja trafia do natywnego atrybutu action formularza, umożliwia to stopniowe ulepszanie funkcjonalności. Należy pamiętać, że wysyłanie danych przed ich przetworzeniem działa tylko wtedy, gdy operacja polega na wywołaniu funkcji serwerowej; zwykła funkcja kliencka wymaga załadowanego JavaScriptu. Ponadto formData.get zwraca null w przypadku braku pola, więc w rzeczywistym kodzie należy to uwzględnić.
Czytanie stanu formularza z dowolnego potomka za pomocą useFormStatus
Klawisz wysyłania umieszczony głęboko w formularzu często musi wiedzieć, czy trwa proces wysyłania danych, a przechodzenie właściwości isPending przez każdą warstwę jest uciążliwe. Funkcja useFormStatus, importowana z react-dom, odczytuje stan najbliższego rodzica typu <form>.
import { useFormStatus } from "react-dom";
SubmitButton nie przyjmuje żadnych właściwości; pobiera stan oczekujący z otaczającego go formularza, co czyni go idealnym dla komponentów przycisków współdzielonych.
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}function SettingsForm({ action }) {
return (
<form action={action}>
<input name="username" />
<SubmitButton />
</form>
);
}
Hook musi być wywoływany w komponencie renderowanym wewnątrz <form>. Wywołanie go w komponencie, który renderuje sam formularz, nie zwraca żadnego stanu oczekującego.
Wcześniejsze pokazywanie oczekiwanych wyników za pomocą useOptimistic
useOptimistic od razu wyświetla przewidywany wynik działania, podczas gdy trwa rzeczywista próba.
import { useOptimistic, useRef } from "react";
W TodoList hook wyprowadza optimisticTodos z właściwości todos, dodając wpisy oznaczone jako pending: true. Działanie formularza dodaje element optymistyczny, resetuje formularz za pomocą ref, a następnie czeka na rzeczywiste działanie serwera. Elementy o statusie oczekującym są renderowane z połowiczną przejrzystością.
function TodoList({ todos, addTodoAction }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const formRef = useRef(null); async function handleSubmit(formData) {
const title = formData.get("title");
addOptimisticTodo({ id: Date.now(), title });
formRef.current?.reset();
await addTodoAction(formData);
} return (
<div>
<form ref={formRef} action={handleSubmit}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
Model mentalny jest precyzyjny: stan optymistyczny istnieje tylko podczas wykonywania akcji. Gdy ta się zakończy, React go usuwa i pokazuje to, co aktualnie zawiera todos. W przypadku sukcesu komponent nadrzędny musi dostarczyć zaktualizowane todos (na przykład poprzez ponowną weryfikację), w przeciwnym razie pozycja znika; przy niepowodzeniu znikają one automatycznie, co stanowi mechanizm cofnięcia. Komunikaty o błędach nadal wymagają obsługi. Artykuł na temat sposobów radzenia sobie z błędami w useOptimistic rollback omawia przypadki krawędziowe.
Bonus: ref jako zwykła właściwość
React 19 umożliwia również komponentom funkcyjnym odbieranie ref jako zwykłej właściwości, dzięki czemu forwardRef jest niepotrzebny w większości przypadków. To eliminuje wiele warstw otaczających biblioteki komponentów.
Główne wnioski
use()rozwija obietnice i kontekst podczas renderowania z użyciem Suspense, ale obietnica musi być stabilna.useActionStatełączy stan, wynik oraz flagę oczekiwania w jeden hook oparty na formularzu.useFormStatusodczytuje stan otaczającego formularza z jego potomka, eliminując konieczność przechodzenia przez poszczególne właściwości.useOptimisticpokazuje oczekiwane wyniki podczas wykonywania akcji i polega na aktualnych właściwościach, aby je utrzymać.- Zanim ponownie stworzysz kombinację
useStateiuseEffect, sprawdź, czy któryś z tych rozwiązań pasuje.
Literatura pokrewna
- Usunięcie ref lastest-value: Zdarzenia efektów z useEffectEvent — Dowiedz się, jak funkcja useEffectEvent w React 19.2 oddziela logikę nierеакtywną od efektów, eliminuje obejścia z użyciem ref lastest-value oraz wskazuje, kiedy ten hook nie jest odpowiednim narzędziem.
- Triaging aktualizacji do React 19: Jakie nowe API zastępują istniejące obejścia — Praktyczny przegląd funkcji use, Server Actions, useOptimistic oraz React Compiler, wraz z istotnymi zastrzeżeniami i planem, które zmiany w React 19 należy wprowadzić najpierw.