1. Почему это вообще работает?
Как вы уже знаете, в Next.js 15 с App Router и Server Actions появился новый, очень удобный паттерн: вы описываете функцию с "use server", а затем просто указываете её как action у формы. Когда пользователь отправляет форму, Next.js автоматически сериализует все данные полей формы и передаёт их на сервер, где вызывает вашу Server Action с этими данными.
Выглядит магически, но под капотом всё честно:
- Браузер отправляет POST-запрос (обычно с Content-Type: application/x-www-form-urlencoded или multipart/form-data).
- Next.js парсит этот запрос и превращает данные формы в объект FormData.
- Ваш Server Action получает этот объект и делает с ним всё, что душе угодно.
Пример базовой формы и Server Action
// actions.js
"use server";
export async function handleForm(formData) {
const name = formData.get('name'); // Получаем значение поля name
const email = formData.get('email');
// Здесь можно делать всё, что угодно: валидация, запись в БД, отправка писем
}
// page.jsx
import { handleForm } from './actions';
export default function ContactForm() {
return (
<form action={handleForm}>
<input name="name" placeholder="Имя" />
<input name="email" placeholder="Email" type="email" />
<button type="submit">Отправить</button>
</form>
);
}
Важно!
Server Action всегда получает первым аргументом объект FormData (или, если используете <input type="file">, то и файлы тоже).
2. Объект FormData: как с ним работать
FormData — это специальный объект, который содержит пары "ключ-значение" для всех полей формы. Если у вас есть поле <input name="name" value="Вася" />, то formData.get('name') вернёт "Вася".
Основные методы FormData
- formData.get(name) — получить значение первого поля с таким именем.
- formData.getAll(name) — получить массив всех значений (актуально для чекбоксов, select multiple и т.п.).
- formData.has(name) — проверить, есть ли поле с таким именем.
Пример: разные типы полей
<form action={handleForm}>
<input name="name" value="Вася" />
<input name="age" value="30" type="number" />
<input name="subscribe" type="checkbox" value="yes" checked />
<input name="colors" value="red" type="checkbox" checked />
<input name="colors" value="green" type="checkbox" />
<button type="submit">Отправить</button>
</form>
export async function handleForm(formData) {
const name = formData.get('name'); // "Вася"
const age = formData.get('age'); // "30" (всегда строка!)
const subscribe = formData.get('subscribe'); // "yes" (если чекбокс отмечен)
const colors = formData.getAll('colors'); // ["red"] (если только red отмечен)
}
Обратите внимание:
Все значения, получаемые из FormData, — это строки (или File, если это файл). Даже если поле типа number, вы получите строку "30". Не забудьте привести к нужному типу!
3. Передача сложных данных: массивы, чекбоксы, select multiple
Массивы (например, несколько чекбоксов)
Если у вас несколько полей с одинаковым именем, например:
<input type="checkbox" name="hobbies" value="reading" />
<input type="checkbox" name="hobbies" value="coding" />
<input type="checkbox" name="hobbies" value="gaming" />
и пользователь выбрал "coding" и "gaming", то:
const hobbies = formData.getAll('hobbies'); // ["coding", "gaming"]
Select multiple
<select name="fruits" multiple>
<option value="apple">Яблоко</option>
<option value="banana">Банан</option>
<option value="cherry">Вишня</option>
</select>
Если выбрано "apple" и "cherry":
const fruits = formData.getAll('fruits'); // ["apple", "cherry"]
Передача файлов
Работа с файлами (например, <input type="file" name="avatar" />) ничем не отличается — в FormData попадёт объект File.
export async function handleForm(formData) {
const avatar = formData.get('avatar'); // File или null
if (avatar && avatar.size > 0) {
// Можно сохранить файл, например, через fs/promises или cloud storage
}
}
Внимание:
- Для передачи файлов форма должна иметь enctype="multipart/form-data".
- В React это не обязательно указывать — браузер сам подставит нужный тип, если есть <input type="file">.
4. Пример: обработка формы регистрации пользователя
Давайте соберём вместе всё, что знаем, и реализуем форму регистрации с несколькими полями, чекбоксом и валидацией на сервере.
// actions.js
"use server";
export async function registerUser(formData) {
const username = formData.get('username')?.trim();
const email = formData.get('email')?.trim();
const password = formData.get('password');
const agree = formData.get('agree'); // "on" если чекбокс отмечен
// Простая валидация
if (!username || !email || !password) {
throw new Error("Все поля обязательны!");
}
if (agree !== "on") {
throw new Error("Необходимо принять условия!");
}
// Здесь можно добавить запись в базу, отправку письма и т.д.
return { success: true, username };
}
// RegisterForm.jsx
import { registerUser } from './actions';
export default function RegisterForm() {
return (
<form action={registerUser}>
<input name="username" placeholder="Имя пользователя" required />
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Пароль" required />
<label>
<input type="checkbox" name="agree" required /> Я принимаю условия
</label>
<button type="submit">Зарегистрироваться</button>
</form>
);
}
5. Как обрабатывать результат Server Action
Server Actions могут возвращать любые сериализуемые данные (например, объект с результатом, ошибкой, новым состоянием). В простом случае результат никак не используется, но с хуками вроде useFormState (см. предыдущие лекции) можно отображать сообщения об успехе или ошибке.
// actions.js
"use server";
export async function registerUser(formData) {
// ...как выше
return { ok: true, message: "Регистрация прошла успешно!" };
}
// RegisterForm.jsx
'use client';
import { useFormState } from 'react-dom';
import { registerUser } from './actions';
export default function RegisterForm() {
const [state, formAction] = useFormState(registerUser, { ok: false });
return (
<form action={formAction}>
{/* ...поля */}
<button type="submit">Зарегистрироваться</button>
{state?.message && <div>{state.message}</div>}
</form>
);
}
6. Особенности и ограничения передачи данных
- Всё, что не <input name="..."> или <select name="...">, не попадёт в FormData. Если забыли указать name, поле не отправится!
- Типы данных: Всё, что не файл, передаётся как строка. Даже если поле <input type="number" ... />, вы получите строку. Не забывайте преобразовывать к нужному типу (Number(value)).
- Checkbox: Если чекбокс не отмечен — его нет в FormData вообще. Если отмечен, то значение — из value, либо "on" по умолчанию.
- Radio: Только выбранный radio попадает в FormData.
- Файлы: Для передачи файлов используйте <input type="file"> и соответствующую обработку на сервере.
- Массивы: Для передачи нескольких значений используйте одинаковое имя для всех полей (name="tags" у каждого чекбокса).
7. Типичные ошибки при передаче данных формы в Server Actions
Ошибка №1: Не указано имя поля (name).
Если у <input> или <select> нет атрибута name, оно не попадёт в FormData. Проверьте внимательно разметку!
Ошибка №2: Ожидание числа, а приходит строка.
Все значения из FormData — строки. Если ждёте число, делайте явное преобразование через Number(value) или parseInt(value, 10).
Ошибка №3: Чекбокс не отмечен — ключа нет в FormData.
Если чекбокс не отмечен, он вообще не попадёт в FormData. Не пытайтесь сделать formData.get('subscribe') === false — лучше проверяйте formData.has('subscribe').
Ошибка №4: Несколько значений (чекбоксы, select multiple) — забыли использовать getAll.
Если у вас несколько чекбоксов с одним именем, используйте formData.getAll('имя'), иначе получите только первое значение.
Ошибка №5: Попытка получить поле, которого нет.
Если такого поля не было, formData.get('что-то') вернёт null. Не забывайте обрабатывать этот случай.
Ошибка №6: Неправильная обработка файлов.
Если используете <input type="file">, не забывайте, что значение — это объект File, а не строка. Проверяйте размер, тип и т.д.
Ошибка №7: Забыли обработать ошибки в Server Action.
Если Server Action выбрасывает ошибку, клиент получит generic сообщение. Для красивой обработки используйте useFormState и возвращайте объект с ошибкой.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ