У 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, текст) під час очікування.