Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP4 min czytania978 słów

Źródła: React 19’s New Hooks: A Practical Guide

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.
  • useFormStatus odczytuje stan otaczającego formularza z jego potomka, eliminując konieczność przechodzenia przez poszczególne właściwości.
  • useOptimistic pokazuje oczekiwane wyniki podczas wykonywania akcji i polega na aktualnych właściwościach, aby je utrzymać.
  • Zanim ponownie stworzysz kombinację useState i useEffect, sprawdź, czy któryś z tych rozwiązań pasuje.

Literatura pokrewna