Розкрийте можливості хука useActionState в React. Дізнайтеся, як він спрощує керування формами, обробляє стани очікування та покращує досвід користувача на детальних практичних прикладах.
React useActionState: вичерпний посібник із сучасного керування формами
Світ веб-розробки постійно еволюціонує, і екосистема React знаходиться в авангарді цих змін. В останніх версіях React представив потужні функції, які кардинально покращують спосіб створення інтерактивних та стійких додатків. Серед найвпливовіших з них — хук useActionState, що змінює правила гри в обробці форм та асинхронних операцій. Цей хук, раніше відомий як useFormState в експериментальних релізах, тепер є стабільним та незамінним інструментом для будь-якого сучасного React-розробника.
Цей вичерпний посібник проведе вас у глибоке занурення в useActionState. Ми розглянемо проблеми, які він вирішує, його основні механізми та як використовувати його разом із допоміжними хуками, такими як useFormStatus, для створення кращого користувацького досвіду. Незалежно від того, чи створюєте ви просту контактну форму, чи складний, насичений даними додаток, розуміння useActionState зробить ваш код чистішим, більш декларативним та надійнішим.
Проблема: складність традиційного керування станом форми
Перш ніж ми зможемо оцінити елегантність useActionState, ми повинні зрозуміти виклики, на які він відповідає. Протягом багатьох років керування станом форми в React включало передбачуваний, але часто громіздкий патерн з використанням хука useState.
Розглянемо поширений сценарій: проста форма для додавання нового продукту до списку. Нам потрібно керувати кількома частинами стану:
- Значенням поля вводу для назви продукту.
- Станом завантаження або очікування, щоб надати користувачеві зворотний зв'язок під час виклику API.
- Станом помилки для відображення повідомлень, якщо відправлення не вдалося.
- Станом успіху або повідомленням після завершення.
Типова реалізація може виглядати приблизно так:
Приклад: «Старий спосіб» з кількома хуками useState
// Вигадана функція API
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Назва продукту повинна містити щонайменше 3 символи.');
}
console.log(`Продукт "${productName}" додано.`);
return { success: true };
};
// Компонент
{error}import { useState } from 'react';
function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);
try {
await addProductAPI(productName);
setProductName(''); // Очистити поле вводу в разі успіху
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'Додавання...' : 'Додати продукт'}
{error &&
);
}
Цей підхід працює, але має кілька недоліків:
- Шаблонний код: Нам потрібно три окремі виклики useState для керування тим, що концептуально є єдиним процесом відправлення форми.
- Ручне керування станом: Розробник несе відповідальність за ручне встановлення та скидання станів завантаження та помилки в правильному порядку всередині блоку try...catch...finally. Це повторювано і схильно до помилок.
- Зв'язаність: Логіка обробки результату відправлення форми тісно пов'язана з логікою рендерингу компонента.
Представляємо useActionState: зміна парадигми
useActionState — це хук React, розроблений спеціально для керування станом асинхронної дії, такої як відправлення форми. Він оптимізує весь процес, пов'язуючи стан безпосередньо з результатом функції дії.
Його сигнатура є чіткою та лаконічною:
const [state, formAction] = useActionState(actionFn, initialState);
Розберемо його компоненти:
actionFn(previousState, formData)
: Це ваша асинхронна функція, яка виконує роботу (наприклад, викликає API). Вона отримує попередній стан та дані форми як аргументи. Важливо, що те, що ця функція повертає, стає новим станом.initialState
: Це значення стану до першого виконання дії.state
: Це поточний стан. Він спочатку містить initialState і оновлюється до значення, що повертається вашою actionFn, після кожного виконання.formAction
: Це нова, обгорнута версія вашої функції дії. Ви повинні передати цю функцію в пропсaction
елемента<form>
. React використовує цю обгорнуту функцію для відстеження стану очікування дії.
Практичний приклад: рефакторинг з useActionState
Тепер давайте проведемо рефакторинг нашої форми продукту за допомогою useActionState. Покращення стає очевидним одразу.
Спочатку нам потрібно адаптувати нашу логіку дії. Замість того, щоб кидати помилки, дія повинна повертати об'єкт стану, який описує результат.
Приклад: «Новий спосіб» з useActionState
// Функція дії, розроблена для роботи з useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // Симуляція мережевої затримки
if (!productName || productName.length < 3) {
return { message: 'Назва продукту повинна містити щонайменше 3 символи.', success: false };
}
console.log(`Продукт "${productName}" додано.`);
// У разі успіху повертаємо повідомлення про успіх і очищуємо форму.
return { message: `Успішно додано "${productName}"`, success: true };
};
// Відрефакторений компонент
{state.message} {state.message}import { useActionState } from 'react';
// Примітка: у наступному розділі ми додамо useFormStatus для обробки стану очікування.
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
Подивіться, наскільки це чистіше! Ми замінили три хуки useState одним хуком useActionState. Відповідальність компонента тепер полягає виключно в рендерингу UI на основі об'єкта `state`. Вся бізнес-логіка акуратно інкапсульована у функції `addProductAction`. Стан оновлюється автоматично залежно від того, що повертає дія.
Але зачекайте, а як щодо стану очікування? Як нам деактивувати кнопку під час відправлення форми?
Обробка станів очікування за допомогою useFormStatus
React надає супутній хук, useFormStatus, розроблений для вирішення саме цієї проблеми. Він надає інформацію про статус останнього відправлення форми, але з одним критичним правилом: його потрібно викликати з компонента, який рендериться всередині <form>
, статус якої ви хочете відстежувати.
Це заохочує чітке розділення відповідальності. Ви створюєте компонент спеціально для елементів UI, які повинні знати про стан відправлення форми, як-от кнопка відправки.
Хук useFormStatus повертає об'єкт з кількома властивостями, найважливішою з яких є `pending`.
const { pending, data, method, action } = useFormStatus();
pending
: Булеве значення, яке є `true`, якщо батьківська форма зараз відправляється, і `false` в іншому випадку.data
: Об'єкт `FormData`, що містить дані, які відправляються.method
: Рядок, що вказує на HTTP-метод (`'get'` або `'post'`).action
: Посилання на функцію, передану в пропс `action` форми.
Створення кнопки відправки, що враховує стан
Давайте створимо окремий компонент `SubmitButton` та інтегруємо його в нашу форму.
Приклад: компонент SubmitButton
import { useFormStatus } from 'react-dom';
// Примітка: useFormStatus імпортується з 'react-dom', а не з 'react'.
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'Додавання...' : 'Додати продукт'}
);
}
Тепер ми можемо оновити наш основний компонент форми, щоб використовувати його.
Приклад: повна форма з useActionState та useFormStatus
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (функція addProductAction залишається без змін)
function SubmitButton() { /* ... як визначено вище ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* Можна додати ключ для скидання поля вводу в разі успіху */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
З такою структурою компоненту `CompleteProductForm` не потрібно нічого знати про стан очікування. `SubmitButton` є повністю самодостатнім. Цей композиційний патерн є неймовірно потужним для створення складних, легких у підтримці UI.
Сила прогресивного поліпшення
Однією з найглибших переваг цього нового підходу, заснованого на діях, особливо при використанні з Server Actions, є автоматичне прогресивне поліпшення. Це життєво важлива концепція для створення додатків для глобальної аудиторії, де мережеві умови можуть бути ненадійними, а користувачі можуть мати старіші пристрої або вимкнений JavaScript.
Ось як це працює:
- Без JavaScript: Якщо браузер користувача не виконує клієнтський JavaScript, `<form action={...}>` працює як стандартна HTML-форма. Вона робить запит на повну сторінку до сервера. Якщо ви використовуєте фреймворк, як-от Next.js, серверна дія виконується, і фреймворк перерендерить всю сторінку з новим станом (наприклад, показуючи помилку валідації). Додаток є повністю функціональним, просто без плавності, притаманної SPA.
- З JavaScript: Як тільки бандл JavaScript завантажується і React гідратує сторінку, та сама `formAction` виконується на стороні клієнта. Замість повного перезавантаження сторінки, вона поводиться як типовий fetch-запит. Дія викликається, стан оновлюється, і перерендериться лише необхідна частина компонента.
Це означає, що ви пишете логіку форми один раз, і вона бездоганно працює в обох сценаріях. Ви створюєте стійкий, доступний додаток за замовчуванням, що є величезною перемогою для користувацького досвіду в усьому світі.
Просунуті патерни та сценарії використання
1. Серверні дії проти клієнтських дій
`actionFn`, яку ви передаєте в useActionState, може бути стандартною клієнтською асинхронною функцією (як у наших прикладах) або серверною дією. Серверна дія — це функція, визначена на сервері, яку можна викликати безпосередньо з клієнтських компонентів. У фреймворках, як-от Next.js, ви визначаєте її, додаючи директиву "use server";
на початку тіла функції.
- Клієнтські дії: Ідеально підходять для мутацій, які впливають лише на стан на стороні клієнта або викликають сторонні API безпосередньо з клієнта.
- Серверні дії: Ідеально підходять для мутацій, що включають базу даних або інші серверні ресурси. Вони спрощують вашу архітектуру, усуваючи необхідність вручну створювати API-ендпоінти для кожної мутації.
Краса в тому, що useActionState працює ідентично з обома. Ви можете замінити клієнтську дію на серверну, не змінюючи код компонента.
2. Оптимістичні оновлення з `useOptimistic`
Для ще більш чутливого відчуття ви можете поєднувати useActionState з хуком useOptimistic. Оптимістичне оновлення — це коли ви оновлюєте UI негайно, *припускаючи*, що асинхронна дія буде успішною. Якщо вона зазнає невдачі, ви повертаєте UI до його попереднього стану.
Уявіть собі додаток соціальної мережі, де ви додаєте коментар. Оптимістично ви б миттєво показали новий коментар у списку, поки запит надсилається на сервер. useOptimistic розроблений для роботи пліч-о-пліч з діями, щоб зробити цей патерн простим у реалізації.
3. Скидання форми після успішного відправлення
Поширеною вимогою є очищення полів форми після успішного відправлення. Існує кілька способів досягти цього за допомогою useActionState.
- Трюк з пропсом `key`: Як показано в нашому прикладі `CompleteProductForm`, ви можете призначити унікальний `key` для поля вводу або всієї форми. Коли ключ змінюється, React демонтує старий компонент і монтує новий, ефективно скидаючи його стан. Прив'язка ключа до прапорця успіху (`key={state.success ? 'success' : 'initial'}`) є простим та ефективним методом.
- Контрольовані компоненти: Ви все ще можете використовувати контрольовані компоненти, якщо це необхідно. Керуючи значенням поля вводу за допомогою useState, ви можете викликати функцію-сетер для його очищення всередині useEffect, який слухає стан успіху від useActionState.
Поширені пастки та найкращі практики
- Розміщення
useFormStatus
: Пам'ятайте, компонент, що викликає useFormStatus, повинен рендеритися як дочірній елемент<form>
. Він не працюватиме, якщо він є сусіднім елементом або батьківським. - Серіалізований стан: При використанні серверних дій об'єкт стану, що повертається з вашої дії, повинен бути серіалізованим. Це означає, що він не може містити функції, символи або інші несеріалізовані значення. Використовуйте прості об'єкти, масиви, рядки, числа та булеві значення.
- Не кидайте помилки в діях: Замість `throw new Error()`, ваша функція дії повинна витончено обробляти помилки та повертати об'єкт стану, який описує помилку (наприклад, `{ success: false, message: 'Сталася помилка' }`). Це забезпечує передбачуване оновлення стану.
- Визначте чітку структуру стану: Встановіть послідовну структуру для вашого об'єкта стану з самого початку. Структура на зразок `{ data: T | null, message: string | null, success: boolean, errors: Record
| null }` може охопити багато сценаріїв використання.
useActionState проти useReducer: швидке порівняння
На перший погляд, useActionState може здатися схожим на useReducer, оскільки обидва включають оновлення стану на основі попереднього стану. Однак вони служать різним цілям.
useReducer
— це хук загального призначення для керування складними переходами стану на клієнтській стороні. Він запускається шляхом диспетчеризації дій і ідеально підходить для логіки стану, що має багато можливих, синхронних змін стану (наприклад, складний багатоетапний майстер).useActionState
— це спеціалізований хук, розроблений для стану, що змінюється у відповідь на одну, зазвичай асинхронну дію. Його основна роль — інтеграція з HTML-формами, серверними діями та функціями конкурентного рендерингу React, такими як переходи стану очікування.
Висновок: для відправлення форм та асинхронних операцій, пов'язаних з формами, useActionState є сучасним, спеціалізованим інструментом. Для інших складних, клієнтських машин стану, useReducer залишається чудовим вибором.
Висновок: приймаючи майбутнє форм у React
Хук useActionState — це більше, ніж просто новий API; він представляє фундаментальний зсув у бік більш надійного, декларативного та орієнтованого на користувача способу обробки форм та мутацій даних у React. Використовуючи його, ви отримуєте:
- Зменшення шаблонного коду: Один хук замінює кілька викликів useState та ручну організацію стану.
- Інтегровані стани очікування: Безшовно обробляйте UI завантаження за допомогою супутнього хука useFormStatus.
- Вбудоване прогресивне поліпшення: Пишіть код, який працює з JavaScript або без нього, забезпечуючи доступність та стійкість для всіх користувачів.
- Спрощена комунікація з сервером: Природно поєднується з серверними діями, оптимізуючи досвід повностекової розробки.
Коли ви починаєте нові проєкти або рефакторите існуючі, подумайте про використання useActionState. Це не тільки покращить ваш досвід розробника, зробивши ваш код чистішим і більш передбачуваним, але й дасть вам змогу створювати якісніші додатки, які є швидшими, стійкішими та доступними для різноманітної глобальної аудиторії.