З React 19 обробка форм стала значно ефективнішою та декларативнішою завдяки введенню таких функцій, як action
, useActionState
та useFormStatus
. Ці вдосконалення сприяють спрощенню поширених шаблонів управління формами, зменшенню кількості шаблонного коду та покращенню досвіду розробників.
Чому форми в React 19 кращі
Підхід React 19 до форм усуває частину ручної роботи, яку розробники раніше виконували. Функції, як-от useActionState
та useFormStatus
, чудово інтегруються з нативним елементом `` та декларативною моделлю React. Це дозволяє більш природно обробляти стани, такі як подача, валідація та помилки, зберігаючи при цьому код компонента чистим та читабельним.
useActionState
: Оптимізація робочих процесів дій форми
React 19 вводить хук useActionState
, який призначений для управління станом дій, що ініціюються формами, таких як подачі чи скидання. Цей хук спрощує процес відстеження асинхронних операцій, надаючи чіткий зворотний зв'язок користувачам під час завантаження або в разі помилок.
Мета: Орієнтований на відстеження стану конкретної серверної дії незалежно від будь-якої конкретної форми.
Обсяг: Обмежений лише самою дією, може бути використаний у будь-якому місці компонента.
Особливості useActionState
:
- Поточний стан. Під час першого рендеру він буде відповідати
initialState
, який ви передали. Після виклику дії він відповідатиме значенню, що повертається цією дією. - Нова дія, яку можна передати як пропс
action
у ваш компонентform
або якformAction
у будь-яку кнопку всередині форми. - Прапорець
isPending
, який вказує, чи є незавершена транзакція.
Приклад: Обробка подачі форми з відгуками
Уявіть собі форму відгуків, де користувачі надсилають свої думки. Використовуючи useActionState
, ви можете безперешкодно керувати процесом подачі.
Створимо нашу форму:
import React, { useActionState } from 'react';
const fetchFeedback = () => {
return new Promise(function (resolve, reject) {
// Встановлюємо час на 2000 мс
setTimeout(resolve, 2000);
}).then(function () {
return 'feedback';
});
};
export default function Form() {
const [state, formAction, isPending] = useActionState(fetchFeedback);
return (
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{!!state && !isPending && (
<p className="success">Thank you for your feedback!</p>
)}
</form>
);
}
fetchFeedback
— це наша асинхронна дія, яку ми передаємо в хук useActionState
.
Коли форма надсилається, викликається функція дії, яку ми передали. Її повернуте значення стає новим поточним станом форми.
Також дія отримує новий перший аргумент — поточний стан форми. Під час першої подачі форми це буде початковий стан, який ви передали, а під час наступних подач — значення, яке повернулося після останнього виклику дії.
useFormStatus
: Поглиблений зворотний зв'язок
React 19 вводить хук useFormStatus
для управління статусом форм під час операцій, таких як подача. Цей хук дозволяє відображати спінери завантаження, повідомлення про помилки або індикатори успіху без необхідності вручну відслідковувати ці стани.
const { pending, data, method, action } = useFormStatus();
Мета: Орієнтований на відстеження статусу поточної подачі форми. Він надає інформацію про життєвий цикл подачі конкретної форми, в межах якої використовується.
Обсяг: Обмежений формою, в межах якої він використовується.
Повертає:
Об'єкт status
з такими властивостями:
pending
: Булеве значення. Якщоtrue
, це означає, що форма чекає подачі. Інакше —false
.data
: Об'єкт, що реалізує інтерфейсFormData
, який містить дані, що подаються формою.
Якщо активної подачі немає або батьківського елемента<form>
також немає, це будеnull
.method
: Рядкове значення, яке може бути'get'
або'post'
. Це означає, чи подає батьківський<form>
дані за допомогою методуGET
чиPOST
HTTP методів. За замовчуванням,<form>
використовує методGET
, який можна задати через властивістьmethod
.action
: Посилання на функцію, передану як пропсaction
батьківському<form>
. Якщо батьківського<form>
немає, властивість будеnull
. Якщо в пропсaction
передано значення URI або пропс не вказано,status.action
будеnull
.
Приклад: Обробка стану завантаження з useFormStatus
import React, { useRef, useState } from 'react';
import { useFormStatus } from 'react-dom';
import './style.css';
const submitUserData = () => {
return new Promise(function (resolve, reject) {
// Встановлюємо час на 2000 мс
setTimeout(resolve, 2000);
}).then(function () {
return {
status: 'success',
result: 'User',
};
});
};
function SubmitButton() {
const { pending, data } = useFormStatus();
return (
<>
{pending ? 'Submitting...' : 'Submit'}
{data ? `Requesting ${data?.get('name')}...` : ''}
); } export default function App() { return (
Name:
Email:
Message:
); } ``` Важлива примітка:
> `useFormStatus` не повертає статусну інформацію для `<form>`, що рендериться в тому ж компоненті.
>
> Хук `useFormStatus` повертає статусну інформацію лише для батьківського `<form>`, а не для будь-якого `<form>`, що рендериться в тому ж компоненті, який викликає цей хук, або для дочірніх компонентів.
**useActionState vs useFormStatus:**

## Коли використовувати кожен хук?
- Використовуйте `useFormStatus`, коли працюєте з подачами форм і вам потрібно відслідковувати статус життєвого циклу форми (наприклад, відключення кнопки подачі під час подачі).
- Використовуйте `useActionState`, коли управляєте серверними діями, які не прив'язані безпосередньо до форми (наприклад, програмні операції збереження даних, фонові API виклики або робочі процеси, що керуються діями).
## Покращення досвіду користувача за допомогою `useTransition`
- Форми з інтенсивною валідацією або API викликами можуть працювати повільно. `useTransition` в React 19 може покращити досвід користувача, відстрочивши не критичні оновлення.
**Приклад**: Уявіть каталог продуктів, де користувачі можуть шукати та фільтрувати продукти в реальному часі. Щоб не блокувати основний потік UI, ми використовуємо `useTransition`, щоб обробляти процес фільтрації як неблокуючу транзакцію.
import React, { useState, useTransition } from 'react';
const products = [
'Laptop',
'Smartphone',
'Headphones',
'Keyboard',
'Mouse',
'Monitor',
'Tablet',
'Charger',
];
async function search(keyword) {
return new Promise((resolve, reject) => {
// Симулюємо повільний запит до мережі
const result = products.filter((product) =>
product.toLowerCase().includes(keyword.toLowerCase())
);
setTimeout(() => {
resolve(result);
}, 2000);
});
}
export default function ProductSearch() {
const [query, setQuery] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const newQuery = e.target.value;
setQuery(newQuery);
// Використовуємо startTransition для обгортання затриманого оновлення
startTransition(async () => {
const result = await search(newQuery);
startTransition(() => {
setFilteredProducts(result);
});
});
};
return (
<>
{isPending ? 'Submitting...' : 'Submit'}
{data ? Requesting ${data?.get('name')}...
: ''}
); } export default function App() { return (
Name:
Email:
Message:
); } ## Чому використовувати `useTransition` тут? Без `useTransition` логіка фільтрації (особливо з великим списком продуктів) могла б заблокувати основний потік UI, зробивши додаток відчутно повільним. Позначивши операцію фільтрації як перехід з низьким пріоритетом, `useTransition` дозволяє додатку залишатись чуйним, поки результати пошуку оновлюються. ; // Порожній масив залежностей гарантує, що це виконається лише один раз при монтуванні return (
## Як це працює: 1. Імітація пагінованих даних: Функція
Paginated List
{isPending &&
Loading page {loadingPage}...
}
{items.map((item, index) => (
{item}
))}
loadPage(currentPage - 1)} disabled={currentPage === 1 || isPending} > Previous loadPage(currentPage + 1)} disabled={ currentPage === Math.ceil(allItems.length / itemsPerPage) || isPending } > Next
); }fetchPage
імітує завантаження даних з API, повертаючи зріз набору даних з імітованою затримкою в 1 секунду. 2. Використання useTransition
: - Хук startTransition
обгортає логіку пагінації, забезпечуючи те, що UI залишається чуйним під час завантаження даних для наступної сторінки. - Стан isPending
використовується для відображення повідомлення "Loading page..." та вимкнення кнопок навігації під час транзакції. 3.
Responsive UI:
Поки користувач переходить між сторінками, решта UI (наприклад, поточні елементи) залишається функціональним, а візуальний зворотний зв'язок ("Loading page...") вказує на процес виконання.
Розширений випадок використання: Завантаження файлів з відслідковуванням прогресу
Завантаження файлів — це поширене завдання в сучасних додатках.
React 19’s useFormStatus
та useTransition
можуть використовуватись для відстеження прогресу завантаження файлів.
useTransition
import React, { useState, useTransition } from 'react';
// Симулюємо завантаження файлу з відслідковуванням прогресу
async function simulateFileUpload(formData, onProgress) {
const file = formData.get('file');
if (!file) throw new Error('No file selected!');
return new Promise((resolve, reject) => {
const fileSize = file.size;
const uploadTime = Math.max(fileSize / 100000, 2000); // Симулюємо час завантаження залежно від розміру файлу
let progress = 0;
const interval = setInterval(() => {
progress += 10;
onProgress(progress); // Оновлюємо прогрес через зворотний виклик
if (progress >= 100) {
clearInterval(interval);
resolve({ success: true, message: 'File uploaded successfully!' });
}
}, uploadTime / 10);
setTimeout(() => {
if (file.size === 0) {
clearInterval(interval);
reject(new Error('File is empty!'));
}
}, uploadTime);
});
}
export default function FileUploadForm() {
const [isPending, startTransition] = useTransition();
const [status, setStatus] = useState({
pending: false,
success: null,
error: null,
progress: 0, // Відслідковуємо прогрес завантаження
});
const handleSubmit = (e) => {
e.preventDefault(); // Запобігаємо стандартній відправці форми
const formData = new FormData(e.target);
setStatus({ pending: true, success: null, error: null, progress: 0 }); // Скидаємо стан перед завантаженням
startTransition(() => {
simulateFileUpload(formData, (progress) => {
setStatus((prev) => ({ ...prev, progress })); // Оновлюємо прогрес динамічно
})
.then((response) => {
setStatus({
pending: false,
success: response,
error: null,
progress: 100,
});
})
.catch((error) => {
setStatus({ pending: false, success: null, error, progress: 0 });
});
});
};
return (
Choose File:
{status.pending ? 'Uploading...' : 'Upload'}
{status.pending && (
Upload Progress: {status.progress}%
// Відображаємо прогрес динамічно )} {status.success && (
{status.success.message}
)} {status.error &&
{status.error.message}
} ); } ``` ; return (
{ e.preventDefault(); // Запобігаємо стандартній відправці форми const formData = new FormData(e.target); simulateFileUpload(formData) .then((response) => formStatus.onSuccess(response)) .catch((error) => formStatus.onError(error)); }} > Choose File: {formStatus.pending ? "Uploading..." : "Upload"}
{formStatus.error &&
{formStatus.error.message}
} {formStatus.success &&
{formStatus.success.message}
} ); } export default FileUploadForm; ``` ## Висновок Використання `useTransition`, `useActionState` та `useFormStatus` в React 19 значно покращує досвід розробки форм.
Вони дозволяють розробникам писати чистіший, більш декларативний код, одночасно надаючи потужні інструменти для управління станом та зворотним зв'язком під час операцій з формами. Завдяки цим функціям ви можете спростити свій код, покращити досвід користувачів та створювати підтримувані додатки.
Перекладено з: [Enhancing Form Handling in React 19: A Look at useTransition, useActionState, and useFormStatus](https://medium.com/@ignatovich.dm/enhancing-form-handling-in-react-19-a-look-at-action-useformstate-and-useformstatus-a5ee68d6bf93)