Пошук уроків, статей та іншого контенту
Створите форми із Server Actions, обробкою відправлення та керуванням станами через Actions і form actions.
Action — це функція, яку React може викликати під час зміни даних: надсилання форми, виконання мутації або іншої асинхронної операції.
У контексті форм action-функція:
отримує FormData;
виконує синхронну або асинхронну операцію;
може повертати результат;
може використовуватися разом зі станами pending, success та error;
може бути серверною функцією, якщо це підтримує фреймворк.
У React 19 функцію можна передати безпосередньо у властивість action елемента <form>:
<form action={submitForm}>
{/* поля форми */}
</form>Під час надсилання форми React викличе submitForm і передасть їй об'єкт FormData.
Розглянемо просту форму, яка обробляється action-функцією.
import { useState } from 'react';
function ContactForm() {
const [message, setMessage] = useState('');
async function sendMessage(formData) {
const name = formData.get('name');
const email = formData.get('email');
if (!name || !email) {
setMessage('Заповніть усі поля');
return;
}
// Імітація запиту до сервера
await new Promise((resolve) => setTimeout(resolve, 1000));
setMessage(`Повідомлення від ${name} надіслано`);
}
return (
<form action={sendMessage}>
<label>
Ім'я
<input name="name" type="text" />
</label>
<label>
Email
<input name="email" type="email" />
</label>
<button type="submit">Надіслати</button>
{message && <p>{message}</p>}
</form>
);
}
export default ContactForm;Кожне поле, значення якого потрібно отримати в action-функції, повинно мати атрибут name.
<input name="email" type="email" />Значення можна отримати за допомогою:
const email = formData.get('email');Для кількох полів з однаковим іменем використовуйте getAll:
const tags = formData.getAll('tags');FormData.get() може повернути string, File або null. Тому значення часто потрібно привести до потрібного типу:
const title = String(formData.get('title') || '').trim();
const age = Number(formData.get('age'));Не варто довіряти даним форми лише через те, що для полів задано HTML-атрибути required, type або min. Сервер також повинен перевіряти отримані дані.
Action-функція зазвичай асинхронна. Користувачу потрібно показати, що операція виконується, і тимчасово заблокувати кнопку.
Для цього використовується хук useFormStatus з пакета react-dom.
useFormStatus повинен викликатися у компоненті, який є дочірнім елементом <form>. Він не працює у тому самому компоненті, де оголошено <form>.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Надсилання...' : 'Надіслати'}
</button>
);
}Повна форма:
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Збереження...' : 'Зберегти'}
</button>
);
}
async function saveProfile(formData) {
const username = String(formData.get('username') || '').trim();
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Збережено:', username);
}
export default function ProfileForm() {
return (
<form action={saveProfile}>
<label>
Ім'я користувача
<input name="username" required />
</label>
<SubmitButton />
</form>
);
}pending буде true від моменту запуску action до завершення його Promise.
useActionStateЯкщо action повинна повертати результат, зручно використовувати useActionState.
Хук має такий загальний вигляд:
const [state, formAction, isPending] = useActionState(
action,
initialState
);Він повертає:
state — останній результат action;
formAction — обгорнуту action-функцію для передачі у <form>;
isPending — ознаку виконання action.
Action, підключена через useActionState, отримує два аргументи:
async function action(previousState, formData) {
// ...
}Перший аргумент — попередній стан, другий — дані форми.
import { useActionState } from 'react';
const initialState = {
message: '',
};
async function subscribe(previousState, formData) {
const email = String(formData.get('email') || '').trim();
if (!email.includes('@')) {
return {
message: 'Введіть коректну email-адресу',
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
message: 'Підписку оформлено',
};
}
export default function SubscribeForm() {
const [state, formAction, isPending] = useActionState(
subscribe,
initialState
);
return (
<form action={formAction}>
<label>
Email
<input name="email" type="email" />
</label>
<button type="submit" disabled={isPending}>
{isPending ? 'Обробка...' : 'Підписатися'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}У цьому прикладі subscribe не передається безпосередньо у action форми. Замість неї використовується formAction, яку повернув useActionState.
Це потрібно для того, щоб React міг:
передати action попередній стан;
зберегти результат action у state;
оновити компонент після завершення операції;
надати стан очікування через isPending.
Зручно повертати зі server action об'єкт із передбачуваною структурою:
{
ok: false,
message: 'Перевірте дані',
errors: {
email: 'Некоректна адреса',
},
}Тоді компонент може показувати загальне повідомлення та помилки окремих полів.
import { useActionState } from 'react';
const initialState = {
ok: false,
message: '',
errors: {},
};
async function createAccount(previousState, formData) {
const email = String(formData.get('email') || '').trim();
const password = String(formData.get('password') || '');
const errors = {};
if (!email.includes('@')) {
errors.email = 'Введіть коректний email';
}
if (password.length < 8) {
errors.password = 'Пароль має містити щонайменше 8 символів';
}
if (Object.keys(errors).length > 0) {
return {
ok: false,
message: 'Форма містить помилки',
errors,
};
}
return {
ok: true,
message: 'Обліковий запис створено',
errors: {},
};
}
export default function AccountForm() {
const [state, formAction, isPending] = useActionState(
createAccount,
initialState
);
return (
<form action={formAction}>
<label>
Email
<input name="email" type="email" aria-invalid={Boolean(state.errors.email)} />
</label>
{state.errors.email && <p>{state.errors.email}</p>}
<label>
Пароль
<input
name="password"
type="password"
aria-invalid={Boolean(state.errors.password)}
/>
</label>
{state.errors.password && <p>{state.errors.password}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Створення...' : 'Створити акаунт'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}Повернення помилок як даних зазвичай краще, ніж використання throw для звичайних помилок валідації. Валідація є очікуваною частиною роботи форми, тому її можна представити у стані.
throw доречний для неочікуваних помилок, наприклад недоступності сервера або порушення внутрішньої умови.
React надає механізм form actions, але спосіб виконання коду на сервері залежить від фреймворку.
Наприклад, у Next.js App Router серверну функцію можна позначити директивою 'use server'. Така функція виконується на сервері, а не в браузері.
Server Action може:
отримати FormData;
перевірити дані;
звернутися до бази даних;
виконати авторизовану операцію;
повернути серіалізований результат.
Файл app/actions.js:
'use server';
export async function createTask(previousState, formData) {
const title = String(formData.get('title') || '').trim();
if (title.length < 3) {
return {
ok: false,
message: 'Назва завдання має містити щонайменше 3 символи',
};
}
// Тут може бути запис до бази даних.
console.log('Нове завдання:', title);
return {
ok: true,
message: 'Завдання створено',
};
}Директива 'use server' повинна бути на початку файлу або функції. У цьому прикладі весь модуль містить серверну action.
Файл app/components/task-form.jsx:
'use client';
import { useActionState } from 'react';
import { createTask } from '../actions';
const initialState = {
ok: false,
message: '',
};
function SubmitButton() {
return (
<button type="submit">
Створити завдання
</button>
);
}
export default function TaskForm() {
const [state, formAction, isPending] = useActionState(
createTask,
initialState
);
return (
<form action={formAction}>
<label>
Назва завдання
<input name="title" required minLength={3} />
</label>
<SubmitButton />
{isPending && <p>Збереження...</p>}
{state.message && (
<p role={state.ok ? 'status' : 'alert'}>
{state.message}
</p>
)}
</form>
);
}Сторінка app/tasks/page.jsx:
import TaskForm from '../components/task-form';
export default function TasksPage() {
return (
<main>
<h1>Новe завдання</h1>
<TaskForm />
</main>
);
}У цьому прикладі:
користувач заповнює форму в браузері;
formAction запускає createTask;
Next.js виконує createTask на сервері;
серверна функція отримує FormData;
результат повертається у state;
компонент показує повідомлення користувачу.
Реальна server action повинна повторно перевіряти всі значення на сервері. HTML-валідація допомагає користувачу, але не є захистом серверної операції.
useFormStatus разом із Server ActionsДля складніших форм стан кнопки можна винести в окремий компонент.
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { createTask } from '../actions';
const initialState = {
ok: false,
message: '',
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Створення...' : 'Створити завдання'}
</button>
);
}
export default function TaskForm() {
const [state, formAction] = useActionState(
createTask,
initialState
);
return (
<form action={formAction}>
<label>
Назва
<input name="title" required />
</label>
<SubmitButton />
{state.message && <p>{state.message}</p>}
</form>
);
}useFormStatus отримує статус найближчої батьківської форми. Це дозволяє повторно використовувати SubmitButton у різних формах без передачі pending через props.
action форми та onSubmitform action і onSubmit вирішують схожі задачі, але мають різну модель використання.
onSubmitfunction Form() {
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
// Обробка даних вручну
}
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button type="submit">Зберегти</button>
</form>
);
}У цьому підході потрібно самостійно:
викликати preventDefault;
створювати FormData;
зберігати стан завантаження;
обробляти результат;
обробляти помилки.
actionasync function save(formData) {
// React передає FormData автоматично
}
function Form() {
return (
<form action={save}>
<input name="title" />
<button type="submit">Зберегти</button>
</form>
);
}Для форм із асинхронною мутацією та useActionState підхід із action часто є компактнішим. onSubmit залишається корисним, коли потрібна нестандартна поведінка події або сумісність із кодом, який уже побудовано навколо подій форми.
Action отримує лише FormData, тому значення можна додати до форми як приховане поле:
<form action={updateTask}>
<input type="hidden" name="taskId" value={task.id} />
<input name="title" defaultValue={task.title} />
<button type="submit">Зберегти</button>
</form>На сервері:
export async function updateTask(previousState, formData) {
const taskId = String(formData.get('taskId') || '');
const title = String(formData.get('title') || '').trim();
// Перевірка taskId і title перед оновленням
return {
ok: true,
message: `Завдання ${taskId} оновлено`,
};
}Приховане поле не є секретом: його значення видно в браузері й користувач може його змінити. Критичні перевірки, зокрема перевірку прав доступу до taskId, необхідно виконувати на сервері.
Для більшості форм із мутацією даних можна використовувати такий алгоритм:
Описати структуру початкового стану.
Отримати значення з FormData.
Перевірити дані в action.
Повернути помилки у відомому форматі.
Виконати серверну операцію, якщо валідація успішна.
Повернути повідомлення про результат.
Показати pending під час виконання.
Заблокувати повторне надсилання форми.
Приклад структури стану:
const initialState = {
ok: false,
message: '',
errors: {},
};Важливо, щоб усі гілки action повертали сумісну структуру. Тоді компонент не повинен перевіряти безліч різних форматів відповіді.
useFormStatus у компоненті формиНеправильно:
function Form() {
const { pending } = useFormStatus();
return (
<form>
<button disabled={pending}>Зберегти</button>
</form>
);
}useFormStatus не бачить форму, оголошену в тому самому компоненті. Винесіть кнопку в дочірній компонент.
name<input type="text" />Таке поле не буде доступним через очікуване ім'я у FormData. Правильно:
<input name="title" type="text" />useActionStateНеправильно:
async function save(formData, previousState) {
// ...
}Правильно:
async function save(previousState, formData) {
// ...
}Після підключення через useActionState першим аргументом є попередній стан.
Атрибути required, minLength і type покращують взаємодію з користувачем, але не захищають server action. Дані потрібно перевіряти повторно на сервері.
Зазвичай достатньо вимкнути кнопку надсилання. Якщо потрібно заблокувати всі поля, це слід робити свідомо, щоб користувач не втратив можливість переглянути або виправити введені дані.
React API для form actions не робить будь-яку JavaScript-функцію серверною. Виконання на сервері потребує фреймворку або іншого середовища, яке підтримує Server Actions.
Action можна передати у властивість action елемента <form>.
React автоматично передає action-функції об'єкт FormData.
useActionState зберігає результат action і надає formAction для форми.
useFormStatus дозволяє отримати стан pending у дочірньому компоненті форми.
Server Actions виконуються на сервері лише за підтримки відповідного фреймворку.
Дані форми потрібно перевіряти на сервері незалежно від HTML-валідації.
Для передбачуваного інтерфейсу action повинна повертати узгоджену структуру стану з повідомленнями та помилками.