Пошук уроків, статей та іншого контенту
Організуєте повернення помилок із Server Action і відобразите їх користувачеві без падіння інтерфейсу.
Server Action виконується на сервері, навіть якщо її викликають із клієнтського компонента. Під час виконання можуть виникнути різні типи помилок:
користувач не заповнив обов’язкове поле;
дані мають неправильний формат;
операція не може бути виконана через бізнес-правила;
сталася неочікувана помилка сервера.
Очікувані помилки, наприклад помилки валідації, не повинні призводити до падіння інтерфейсу. Замість цього Server Action може повернути серіалізований об’єкт зі статусом і повідомленнями, а компонент відобразить їх користувачеві.
форма → Server Action → результат { помилки або успіх } → оновлення UIServer Action може повертати звичайний об’єкт. Найзручніше описати його як стан дії:
type ActionState = {
status: 'idle' | 'error' | 'success'
message?: string
errors?: {
email?: string
password?: string
}
}Такий об’єкт має містити лише дані, які можна серіалізувати: рядки, числа, логічні значення, масиви та звичайні об’єкти.
Не варто повертати об’єкт Error безпосередньо:
// Не рекомендовано
return {
status: 'error',
error: new Error('Невірні дані'),
}Натомість поверніть повідомлення або окремі помилки полів:
return {
status: 'error',
message: 'Перевірте введені дані',
errors: {
email: 'Введіть коректну електронну адресу',
},
}useActionStateДля отримання результату Server Action у клієнтському компоненті використовується хук useActionState з React.
Він повертає:
поточний стан дії;
функцію, яку потрібно передати у властивість action форми;
у новіших версіях React також може повертатися додаткове значення для підрахунку викликів.
Типовий виклик має такий вигляд:
const [state, formAction] = useActionState(serverAction, initialState)Після відправлення форми Next.js передасть у Server Action:
попередній стан;
об’єкт FormData.
У цьому прикладі форма входу перевіряє адресу електронної пошти та пароль. Помилки валідації повертаються у стані Server Action і відображаються біля відповідних полів.
Файл app/login/actions.ts:
'use server'
export type ActionState = {
status: 'idle' | 'error' | 'success'
message?: string
errors?: {
email?: string
password?: string
}
}
export const initialState: ActionState = {
status: 'idle',
}
export async function login(
_previousState: ActionState,
formData: FormData,
): Promise<ActionState> {
const email = String(formData.get('email') ?? '').trim()
const password = String(formData.get('password') ?? '')
const errors: ActionState['errors'] = {}
if (!email) {
errors.email = 'Введіть адресу електронної пошти'
} else if (!email.includes('@')) {
errors.email = 'Введіть коректну адресу електронної пошти'
}
if (!password) {
errors.password = 'Введіть пароль'
} else if (password.length < 8) {
errors.password = 'Пароль має містити щонайменше 8 символів'
}
if (errors.email || errors.password) {
return {
status: 'error',
message: 'Перевірте введені дані',
errors,
}
}
try {
// Тут зазвичай виконується запит до бази даних або іншого сервісу.
// У прикладі перевірка успішно завершується без зовнішніх залежностей.
const isCredentialsValid = true
if (!isCredentialsValid) {
return {
status: 'error',
message: 'Неправильна електронна пошта або пароль',
}
}
return {
status: 'success',
message: 'Вхід успішно виконано',
}
} catch (error) {
// Деталі помилки записуємо на сервері, але не показуємо користувачеві.
console.error('Помилка під час входу:', error)
return {
status: 'error',
message: 'Не вдалося виконати вхід. Спробуйте ще раз.',
}
}
}Файл app/login/LoginForm.tsx:
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { initialState, login } from './actions'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Виконується вхід...' : 'Увійти'}
</button>
)
}
export default function LoginForm() {
const [state, formAction] = useActionState(login, initialState)
return (
<form action={formAction} noValidate>
<div>
<label htmlFor="email">Електронна пошта</label>
<input
id="email"
name="email"
type="email"
aria-invalid={Boolean(state.errors?.email)}
aria-describedby={state.errors?.email ? 'email-error' : undefined}
/>
{state.errors?.email && (
<p id="email-error" role="alert">
{state.errors.email}
</p>
)}
</div>
<div>
<label htmlFor="password">Пароль</label>
<input
id="password"
name="password"
type="password"
aria-invalid={Boolean(state.errors?.password)}
aria-describedby={
state.errors?.password ? 'password-error' : undefined
}
/>
{state.errors?.password && (
<p id="password-error" role="alert">
{state.errors.password}
</p>
)}
</div>
{state.message && (
<p role="status" aria-live="polite">
{state.message}
</p>
)}
<SubmitButton />
</form>
)
}Файл app/login/page.tsx:
import LoginForm from './LoginForm'
export default function LoginPage() {
return (
<main>
<h1>Вхід</h1>
<LoginForm />
</main>
)
}Після відправлення форми відбувається такий процес:
formAction запускає Server Action login.
Server Action читає значення з FormData.
Якщо дані неправильні, повертається об’єкт зі статусом error.
useActionState оновлює state.
Компонент показує повідомлення, не перезавантажуючи сторінку і не падаючи.
Якщо операція успішна, повертається статус success.
Не кожна помилка має оброблятися однаково.
Це ситуації, які є частиною нормальної логіки застосунку:
порожнє поле;
неправильний формат email;
занадто короткий пароль;
користувач із таким email уже існує;
неправильні облікові дані.
Такі ситуації краще повертати як результат Server Action:
return {
status: 'error',
message: 'Користувач із таким email уже існує',
}Це проблеми інфраструктури або програмного коду:
база даних недоступна;
зовнішній сервіс не відповідає;
у коді виникла непередбачена помилка.
Таку помилку потрібно:
записати в серверний лог;
не показувати користувачеві технічні деталі;
повернути загальне повідомлення, якщо ситуацію можна безпечно обробити.
try {
// Операція, яка може завершитися помилкою
await saveData()
} catch (error) {
console.error('Не вдалося зберегти дані:', error)
return {
status: 'error',
message: 'Не вдалося зберегти дані. Спробуйте пізніше.',
}
}Повідомлення на кшталт ECONNREFUSED, SQL-запитів або stack trace не повинні потрапляти в інтерфейс користувача.
Повернення об’єкта підходить для помилок, які очікуються і мають бути показані у формі.
Іноді помилку потрібно викинути через throw. Наприклад, якщо компонент не може відобразити потрібну сторінку або сталася критична помилка, яку не можна коректно представити як стан форми.
Викинута помилка не стає значенням state. Її оброблення відбувається механізмами помилок Next.js, зокрема спеціальними error boundary.
Для помилок валідації зазвичай не потрібно використовувати throw:
// Невдалий підхід для звичайної помилки форми
throw new Error('Email має неправильний формат')Краще повернути структурований результат:
return {
status: 'error',
errors: {
email: 'Email має неправильний формат',
},
}Так користувач залишиться на сторінці, побачить помилку і зможе виправити значення.
Не всі помилки стосуються конкретного поля. Наприклад, сервер може відхилити операцію через бізнес-правило:
return {
status: 'error',
message: 'Неправильна електронна пошта або пароль',
}У компоненті таке повідомлення можна показати окремим блоком:
{state.status === 'error' && state.message && (
<p role="alert">{state.message}</p>
)}Для успішного результату можна використовувати інший стиль або окремий елемент:
{state.status === 'success' && state.message && (
<p role="status">{state.message}</p>
)}Перевірка state.status допомагає не змішувати повідомлення про помилку з повідомленнями про успішне завершення.
Errorreturn {
error,
}Об’єкт помилки не є хорошим форматом результату для UI. До того ж Server Action має повертати дані, які можна передати між сервером і клієнтом.
Повертайте безпечне повідомлення:
return {
status: 'error',
message: 'Операцію не вдалося виконати',
}Не показуйте error.message, якщо він може містити внутрішню інформацію про сервер, базу даних або зовнішній сервіс.
catch (error) {
console.error(error)
return {
status: 'error',
message: 'Виникла внутрішня помилка сервера',
}
}useActionState потребує початкового стану, який має відповідати типу результату дії:
const initialState = {
status: 'idle' as const,
}Початковий стан дозволяє компоненту безпечно працювати до першого відправлення форми.
Якщо Server Action використовується разом із useActionState, першим параметром є попередній стан, а другим — FormData:
export async function action(
previousState: ActionState,
formData: FormData,
) {
// ...
}Якщо переплутати порядок параметрів, замість FormData можна отримати об’єкт стану.
Не варто визначати результат лише за наявністю message, оскільки повідомлення може бути і при помилці, і при успіху.
Краще явно зберігати статус:
{
status: 'error',
message: 'Перевірте дані',
}{
status: 'success',
message: 'Дані збережено',
}Server Action може повертати серіалізований об’єкт із результатом операції.
Для обробки результату форми зручно використовувати useActionState.
Очікувані помилки потрібно повертати у стані дії, а не викидати через throw.
Помилки окремих полів варто зберігати в об’єкті errors.
Загальні повідомлення можна зберігати у властивості message.
Неочікувані помилки потрібно логувати на сервері й замінювати безпечним повідомленням для користувача.
Чіткий статус idle, error або success спрощує відображення стану форми.