Дізнайтеся про хук useActionState в React для оптимізованого керування станом, що ініціюється асинхронними діями. Покращуйте ефективність та UX вашого застосунку.
Реалізація React useActionState: Керування станом на основі дій
Хук useActionState в React, представлений у нових версіях, пропонує вдосконалений підхід до керування оновленнями стану, що є результатом асинхронних дій. Цей потужний інструмент спрощує процес обробки мутацій, оновлення UI та керування станами помилок, особливо при роботі з React Server Components (RSC) та серверними діями. У цьому посібнику ми розглянемо тонкощі useActionState, надаючи практичні приклади та найкращі практики для його реалізації.
Розуміння потреби в керуванні станом на основі дій
Традиційне керування станом у React часто включає окреме керування станами завантаження та помилок всередині компонентів. Коли дія (наприклад, відправка форми, отримання даних) ініціює оновлення стану, розробники зазвичай керують цими станами за допомогою кількох викликів useState та потенційно складної умовної логіки. useActionState надає чистіше та більш інтегроване рішення.
Розглянемо простий сценарій відправки форми. Без useActionState, у вас могли б бути:
- Змінна стану для даних форми.
- Змінна стану для відстеження, чи відправляється форма (стан завантаження).
- Змінна стану для зберігання повідомлень про помилки.
Цей підхід може призвести до громіздкого коду та потенційних невідповідностей. useActionState об'єднує ці аспекти в один хук, спрощуючи логіку та покращуючи читабельність коду.
Представляємо useActionState
Хук useActionState приймає два аргументи:
- Асинхронну функцію («дія»), яка виконує оновлення стану. Це може бути серверна дія або будь-яка інша асинхронна функція.
- Початкове значення стану.
Він повертає масив, що містить два елементи:
- Поточне значення стану.
- Функцію для виклику дії (dispatch). Ця функція автоматично керує станами завантаження та помилок, пов'язаними з дією.
Ось базовий приклад:
import { useActionState } from 'react';
async function updateServer(prevState, formData) {
// Симулюємо асинхронне оновлення на сервері.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
return 'Не вдалося оновити сервер.';
}
return `Ім'я оновлено на: ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'Початковий стан');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
У цьому прикладі:
updateServer— це асинхронна дія, що симулює оновлення на сервері. Вона отримує попередній стан та дані форми.useActionStateініціалізує стан зі значенням 'Початковий стан' та повертає поточний стан і функціюdispatch.- Функція
handleSubmitвикликаєdispatchз даними форми.useActionStateавтоматично обробляє стани завантаження та помилок під час виконання дії.
Обробка станів завантаження та помилок
Однією з ключових переваг useActionState є вбудоване керування станами завантаження та помилок. Функція dispatch повертає проміс, який вирішується з результатом дії. Якщо дія викидає помилку, проміс відхиляється з цією помилкою. Ви можете використовувати це для відповідного оновлення UI.
Змінимо попередній приклад, щоб відображати повідомлення про завантаження та помилку:
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// Симулюємо асинхронне оновлення на сервері.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Не вдалося оновити сервер.');
}
return `Ім'я оновлено на: ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'Початковий стан');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
setIsSubmitting(true);
setErrorMessage(null);
try {
const result = await dispatch(formData);
console.log(result);
} catch (error) {
console.error("Помилка під час відправки:", error);
setErrorMessage(error.message);
} finally {
setIsSubmitting(false);
}
}
return (
);
}
Ключові зміни:
- Ми додали змінні стану
isSubmittingтаerrorMessageдля відстеження станів завантаження та помилок. - У
handleSubmitми встановлюємоisSubmittingвtrueперед викликомdispatchі перехоплюємо будь-які помилки для оновленняerrorMessage. - Ми вимикаємо кнопку відправки під час процесу та умовно відображаємо повідомлення про завантаження та помилки.
Використання useActionState із серверними діями в React Server Components (RSC)
useActionState особливо ефективний при використанні з React Server Components (RSC) та серверними діями. Серверні дії — це функції, які виконуються на сервері і можуть безпосередньо змінювати джерела даних. Вони дозволяють виконувати операції на стороні сервера без написання API-ендпоінтів.
Примітка: Цей приклад вимагає середовища React, налаштованого для серверних компонентів та серверних дій.
// app/actions.js (Серверна дія)
'use server';
import { cookies } from 'next/headers'; //Приклад для Next.js
export async function updateName(prevState, formData) {
const name = formData.get('name');
if (!name) {
return 'Будь ласка, введіть ім\'я.';
}
try {
// Симулюємо оновлення бази даних.
await new Promise(resolve => setTimeout(resolve, 1000));
cookies().set('userName', name);
return `Ім'я оновлено на: ${name}`; //Успіх!
} catch (error) {
console.error("Не вдалося оновити базу даних:", error);
return 'Не вдалося оновити ім\'я.'; // Важливо: повертайте повідомлення, а не викидайте помилку
}
}
// app/page.jsx (Серверний компонент React)
'use client';
import { useActionState } from 'react';
import { updateName } from './actions';
function MyComponent() {
const [state, dispatch] = useActionState(updateName, 'Початковий стан');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
export default MyComponent;
У цьому прикладі:
updateName— це серверна дія, визначена вapp/actions.js. Вона отримує попередній стан та дані форми, оновлює базу даних (симульовано) і повертає повідомлення про успіх або помилку. Ключовий момент: дія повертає повідомлення, а не викидає помилку. Серверні дії віддають перевагу поверненню інформативних повідомлень.- Компонент позначений як клієнтський (
'use client'), щоб використовувати хукuseActionState. - Функція
handleSubmitвикликаєdispatchз даними форми.useActionStateавтоматично керує оновленням стану на основі результату серверної дії.
Важливі аспекти серверних дій
- Обробка помилок у серверних діях: Замість викидання помилок, повертайте змістовне повідомлення про помилку з вашої серверної дії.
useActionStateрозглядатиме це повідомлення як новий стан. Це дозволяє елегантно обробляти помилки на клієнті. - Оптимістичні оновлення: Серверні дії можна використовувати з оптимістичними оновленнями для покращення сприйняття продуктивності. Ви можете негайно оновити UI і відкотити зміни, якщо дія завершиться невдало.
- Ревалідація: Після успішної мутації розгляньте можливість ревалідації кешованих даних, щоб UI відображав актуальний стан.
Просунуті техніки використання useActionState
1. Використання редьюсера для складних оновлень стану
Для більш складної логіки стану ви можете поєднати useActionState з функцією-редьюсером. Це дозволяє керувати оновленнями стану передбачуваним та підтримуваним способом.
import { useActionState } from 'react';
import { useReducer } from 'react';
const initialState = {
count: 0,
message: 'Початковий стан',
};
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
}
async function updateState(state, action) {
// Симулюємо асинхронну операцію.
await new Promise(resolve => setTimeout(resolve, 500));
switch (action.type) {
case 'INCREMENT':
return reducer(state, action);
case 'DECREMENT':
return reducer(state, action);
case 'SET_MESSAGE':
return reducer(state, action);
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useActionState(updateState, initialState);
return (
Кількість: {state.count}
Повідомлення: {state.message}
);
}
2. Оптимістичні оновлення з useActionState
Оптимістичні оновлення покращують досвід користувача, негайно оновлюючи UI так, ніби дія була успішною, а потім відкочуючи оновлення, якщо дія зазнає невдачі. Це може зробити ваш застосунок більш чутливим до дій користувача.
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// Симулюємо асинхронне оновлення на сервері.
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Не вдалося оновити сервер.');
}
return `Ім'я оновлено на: ${data.name}`;
}
function MyComponent() {
const [name, setName] = useState('Початкове ім\'я');
const [state, dispatch] = useActionState(async (prevName, newName) => {
try {
const result = await updateServer(prevName, {
name: newName,
});
return newName; // Оновити у разі успіху
} catch (error) {
// Відкотити у разі помилки
console.error("Оновлення не вдалося:", error);
setName(prevName);
return prevName;
}
}, name);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const newName = formData.get('name');
setName(newName); // Оптимістично оновлюємо UI
await dispatch(newName);
}
return (
);
}
3. Дебаунсинг дій
У деяких сценаріях ви можете захотіти застосувати дебаунсинг до дій, щоб запобігти їх занадто частому виклику. Це може бути корисно для сценаріїв, таких як поля пошуку, де ви хочете викликати дію тільки після того, як користувач припинив вводити текст протягом певного періоду.
import { useActionState } from 'react';
import { useState, useEffect } from 'react';
async function searchItems(prevState, query) {
// Симулюємо асинхронний пошук.
await new Promise(resolve => setTimeout(resolve, 500));
return `Результати пошуку для: ${query}`;
}
function MyComponent() {
const [query, setQuery] = useState('');
const [state, dispatch] = useActionState(searchItems, 'Початковий стан');
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query) {
dispatch(query);
}
}, 300); // Дебаунс 300 мс
return () => clearTimeout(timeoutId);
}, [query, dispatch]);
return (
setQuery(e.target.value)}
/>
Стан: {state}
);
}
Найкращі практики для useActionState
- Зберігайте чистоту дій: Переконайтеся, що ваші дії є чистими функціями (або якомога ближчими до них). Вони не повинні мати побічних ефектів, окрім оновлення стану.
- Елегантно обробляйте помилки: Завжди обробляйте помилки у ваших діях і надавайте користувачеві інформативні повідомлення про помилки. Як зазначалося вище для серверних дій, віддавайте перевагу поверненню рядка з повідомленням про помилку із серверної дії, а не викиданню помилки.
- Оптимізуйте продуктивність: Пам'ятайте про наслідки для продуктивності ваших дій, особливо при роботі з великими наборами даних. Розгляньте використання технік мемоізації, щоб уникнути непотрібних перерендерів.
- Враховуйте доступність: Переконайтеся, що ваш застосунок залишається доступним для всіх користувачів, включаючи людей з обмеженими можливостями. Надавайте відповідні атрибути ARIA та клавіатурну навігацію.
- Ретельне тестування: Пишіть юніт-тести та інтеграційні тести, щоб переконатися, що ваші дії та оновлення стану працюють коректно.
- Інтернаціоналізація (i18n): Для глобальних застосунків впроваджуйте i18n для підтримки кількох мов та культур.
- Локалізація (l10n): Адаптуйте ваш застосунок до конкретних локалей, надаючи локалізований контент, формати дат та символи валют.
useActionState проти інших рішень для керування станом
Хоча useActionState надає зручний спосіб керування оновленнями стану на основі дій, він не є заміною для всіх рішень з керування станом. Для складних застосунків з глобальним станом, який потрібно спільно використовувати між багатьма компонентами, бібліотеки, такі як Redux, Zustand або Jotai, можуть бути більш доцільними.
Коли використовувати useActionState:
- Оновлення стану простої та середньої складності.
- Оновлення стану, тісно пов'язані з асинхронними діями.
- Інтеграція з React Server Components та серверними діями.
Коли варто розглянути інші рішення:
- Складне керування глобальним станом.
- Стан, який потрібно спільно використовувати у великій кількості компонентів.
- Розширені функції, такі як відлагодження з подорожами в часі (time-travel debugging) або проміжне програмне забезпечення (middleware).
Висновок
Хук useActionState від React пропонує потужний та елегантний спосіб керування оновленнями стану, що ініціюються асинхронними діями. Консолідуючи стани завантаження та помилок, він спрощує код та покращує читабельність, особливо при роботі з React Server Components та серверними діями. Розуміння його сильних сторін та обмежень дозволяє вам обрати правильний підхід до керування станом для вашого застосунку, що веде до більш підтримуваного та ефективного коду.
Дотримуючись найкращих практик, викладених у цьому посібнику, ви зможете ефективно використовувати useActionState для покращення досвіду користувача та робочого процесу розробки вашого застосунку. Пам'ятайте, що потрібно враховувати складність вашого застосунку та обирати рішення для керування станом, яке найкраще відповідає вашим потребам. Від простих відправок форм до складних мутацій даних, useActionState може бути цінним інструментом у вашому арсеналі розробника React.