Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як тестувати форми на Actions React 19 з useActionState?

У React 19 форма може передати функцію-дію в action, а useActionState дає результат дії й стан очікування:

function ApplyForm({ submit }: { submit: (email: string) => Promise<string | null> }) {
  const [error, formAction, isPending] = useActionState(async (_prev: string | null, formData: FormData) => {
    return submit(String(formData.get('email')));   // null - успіх, рядок - помилка
  }, null);

  return (
    <form action={formAction}>
      <label>
        Email
        <input name="email" type="email" />
      </label>
      <button disabled={isPending}>{isPending ? 'Надсилаємо...' : 'Надіслати'}</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

Тест - як дії користувача, з очікуванням результату:

test('показує помилку від сервера', async () => {
  const user = userEvent.setup();
  const submit = vi.fn().mockResolvedValue('Ця адреса вже подала заявку');
  render(<ApplyForm submit={submit} />);

  await user.type(screen.getByLabelText('Email'), 'olia@example.com');
  await user.click(screen.getByRole('button', { name: 'Надіслати' }));

  expect(await screen.findByRole('alert')).toHaveTextContent('Ця адреса вже подала заявку');
  expect(submit).toHaveBeenCalledWith('olia@example.com');
});

Що відрізняється від форм з onSubmit:

  • дія асинхронна й виконується в переході (transition) - результат з'являється не одразу після кліку, тому findBy/waitFor, а не синхронна перевірка;
  • поля некеровані - значення беруться з FormData, тож у тесті потрібен реальний name на полі й введення через userEvent;
  • після успішної дії React скидає некеровану форму - поля очищаються. Це варто перевірити, якщо поведінка важлива.

Стан очікування перевіряється, якщо дія не завершується одразу:

let resolve!: (v: string | null) => void;
const submit = vi.fn(() => new Promise<string | null>((r) => (resolve = r)));
render(<ApplyForm submit={submit} />);
await user.click(screen.getByRole('button'));

expect(await screen.findByRole('button', { name: 'Надсилаємо...' })).toBeDisabled();
resolve(null);
await waitFor(() => expect(screen.getByRole('button', { name: 'Надіслати' })).toBeEnabled());

Server Functions ('use server' у фреймворках) у тестах компонентів підміняють через параметри чи vi.mock модуля - сама серверна логіка тестується окремо, на сервері. Повний цикл форма → сервер → відповідь краще перевіряти e2e-тестом.

useFormStatus у дочірній кнопці перевіряється так само - через її вигляд (disabled, текст) під час очікування.

Докладніше в документації: useActionState

Схожі питання