Главная / Статьи

Асинхронные формы в React 19 с use, useActionState и useOptimistic

Как функции use(), useActionState, useFormStatus и useOptimistic заменяют ручную загрузку данных и флаги ошибок в React 19, а также какие подводные камни тайно скрывает каждый из этих хуков.

Издатель: REACTAPP.TOP4 мин чтения978 слов

Источники: React 19’s New Hooks: A Practical Guide

До React 19 практически каждый запрос или форма требовали одинаковой структуры: useState для хранения данных, useEffect для их загрузки, а также флаги для отображения состояния загрузки, ошибок и результатов. React 19 вводит четыре API, которые переносят эту логику в сам фреймворк: use(), useActionState, useFormStatus и useOptimistic. Здесь описано, что делает каждый из них, как они взаимодействуют между собой, а также ограничения, которые легко упустить. Для более полного обзора обновлений см. React 19 upgrade triage.

Чтение обещаний во время отрисовки с помощью use()

use() — это не хук в традиционном смысле. В отличие от хуков, его можно вызывать внутри условий, циклов или после досрочного возврата. Он считывает значение Promise или Context во время отрисовки и сотрудничает с Suspense для отображения состояния ожидания.

import { use, Suspense } from "react";

UserCard принимает promise и распаковывает его с помощью use; пока он не реализован, ближайший элемент Suspense в Page отображает свой запасной вариант.

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>
  );
}

Родительский компонент создаёт promise, дочерний компонент им пользуется, а Suspense отвечает за отображение состояния загрузки. Существует одно важное ограничение: promise должен быть стабильным. Promise, созданный во время отрисовки клиентского компонента, становится новым при каждой отрисовке и приводит к повторному вводу компонента в состояние ожидания, поэтому его следует создавать в серверном компоненте, в кэшируемом загрузчике или через API данных фреймворка.

Обработка отправки данных с useActionState

useActionState заменяет вручную управляемые состояния isSubmitting, error и result. Вы передаёте действие и начальное состояние; в результате получаете текущее состояние, обёрнутое действие для формы и флаг ожидания. Действие получает предыдущее состояние и объект FormData, а то, что оно возвращает, становится следующим состоянием.

import { useActionState } from "react";

Здесь submitFeedback проверяет сообщение, сохраняет его и возвращает либо маркер ошибки, либо маркер успешного выполнения, который форма отображает непосредственно.

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>
  );
}

Поскольку действие передаётся в стандартное атрибут формы action, это обеспечивает прогрессивное улучшение функционала. Обратите внимание, что отправка данных до их обработки работает только тогда, когда действие представляет собой серверную функцию; обычная клиентская функция требует загрузки JavaScript. Кроме того, метод formData.get возвращает null для отсутствующего поля, поэтому в реальном коде необходимо учитывать этот случай.

Чтение статуса формы из любого потомка с помощью useFormStatus

Кнопка отправки, расположенная глубоко внутри формы, часто должна знать, выполняется ли отправка данных, и передача свойства isPending через каждый уровень структуры является громоздкой. Функция useFormStatus, импортируемая из react-dom, позволяет получить статус ближайшего родителя-элемента <form>.

import { useFormStatus } from "react-dom";

SubmitButton не принимает никаких параметров; он берет информацию о состоянии в ожидании из формы, в которой находится, что делает его идеальным решением для компонентов кнопок, используемых совместно.

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>
  );
}

Хук необходимо вызывать в компоненте, отрисовываемом внутри <form>. Если его вызывать в компоненте, отрисовывающем саму форму, информация о состоянии в ожидании не будет получена.

Раннее отображение ожидаемых результатов с useOptimistic

useOptimistic сразу отображает предполагаемый результат действия во время выполнения реального запроса.

import { useOptimistic, useRef } from "react";

В TodoList хук формирует массив optimisticTodos на основе параметра todos, добавляя элементы с меткой pending: true. Действие формы добавляет такой оптимистичный элемент, сбрасывает форму с помощью ref, а затем ожидает реального ответа от сервера. Элементы в состоянии ожидания отображаются с полупрозрачностью.

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>
  );
}

Модель мышления ясна: оптимистичное состояние существует только во время выполнения действия. Как только оно завершается, React удаляет его и отображает то, что в настоящее время хранится в переменной todos. При успешном выполнении родительский компонент должен предоставить обновленные данные todos (например, путем повторной валидации), иначе элемент исчезает; при неудаче он автоматически удаляется, что и является механизмом отката. Сообщения об ошибках также требуют обработки. Статья о способах обработки сбоев при использовании оптимистичного отката рассматривает крайние случаи.

Бонус: ref как обычное свойство

В React 19 функциональные компоненты также могут принимать ref в качестве обычного свойства, поэтому в большинстве случаев forwardRef не требуется. Это позволяет избавиться от множества слоев оберток в библиотеках компонентов.

Основные выводы

  • use() разворачивает обещания и контекст во время отрисовки с использованием Suspense, но обещание должно быть стабильным.
  • useActionState объединяет состояние, результат и флаг ожидания в один хук, основанный на форме.
  • useFormStatus считывает статус внешней формы из её потомка, устраняя необходимость прохождения по свойствам вверх.
  • useOptimistic отображает ожидаемые результаты во время выполнения действия и опирается на свежие свойства для их сохранения.
  • Перед пересозданием комбинации useState и useEffect проверьте, подходит ли к случаю один из этих вариантов.