Головна / Статті

Асинхронні форми в 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 (наприклад, шляхом перевірки), інакше елемент зникає; у разі невдачі він автоматично зникає, що і є механізмом відкату. Повідомлення про помилки все одно потребують обробки. Стаття про режими невдач useOptimistic rollback розглядає крайні випадки.

Бонус: ref як звичайний проп

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

Ключові висновки

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