Odkryj moc hooka useActionState w React. Dowiedz się, jak upraszcza zarządzanie formularzami, obsługuje stany oczekiwania i poprawia UX dzięki praktycznym przykładom.
React useActionState: Kompleksowy przewodnik po nowoczesnym zarządzaniu formularzami
Świat web developmentu nieustannie ewoluuje, a ekosystem Reacta jest na czele tej zmiany. W najnowszych wersjach React wprowadził potężne funkcje, które fundamentalnie ulepszają sposób, w jaki budujemy interaktywne i odporne na błędy aplikacje. Jedną z najbardziej wpływowych z nich jest hook useActionState, który rewolucjonizuje obsługę formularzy i operacji asynchronicznych. Ten hook, wcześniej znany jako useFormState w wersjach eksperymentalnych, jest teraz stabilnym i niezbędnym narzędziem dla każdego nowoczesnego dewelopera Reacta.
Ten kompleksowy przewodnik zabierze Cię w głąb useActionState. Zbadamy problemy, które rozwiązuje, jego podstawowe mechanizmy oraz jak wykorzystać go w połączeniu z uzupełniającymi hookami, takimi jak useFormStatus, aby tworzyć doskonałe doświadczenia użytkownika. Niezależnie od tego, czy budujesz prosty formularz kontaktowy, czy złożoną aplikację intensywnie korzystającą z danych, zrozumienie useActionState sprawi, że Twój kod będzie czystszy, bardziej deklaratywny i solidniejszy.
Problem: Złożoność tradycyjnego zarządzania stanem formularza
Zanim docenimy elegancję useActionState, musimy najpierw zrozumieć wyzwania, którym stawia czoła. Przez lata zarządzanie stanem formularza w React wiązało się z przewidywalnym, ale często uciążliwym wzorcem wykorzystującym hook useState.
Rozważmy typowy scenariusz: prosty formularz do dodawania nowego produktu do listy. Musimy zarządzać kilkoma elementami stanu:
- Wartością pola do wprowadzania nazwy produktu.
- Stanem ładowania lub oczekiwania, aby dać użytkownikowi informację zwrotną podczas wywołania API.
- Stanem błędu, aby wyświetlać komunikaty, jeśli przesłanie się nie powiedzie.
- Stanem sukcesu lub komunikatem po zakończeniu.
Typowa implementacja mogłaby wyglądać mniej więcej tak:
Przykład: 'Stary sposób' z wieloma hookami useState
// Fikcyjna funkcja API
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Nazwa produktu musi mieć co najmniej 3 znaki.');
}
console.log(`Produkt "${productName}" dodany.`);
return { success: true };
};
// Komponent
{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(''); // Wyczyść pole po sukcesie
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'Dodawanie...' : 'Dodaj produkt'}
{error &&
);
}
To podejście działa, ale ma kilka wad:
- Nadmiarowy kod: Potrzebujemy trzech osobnych wywołań useState, aby zarządzać czymś, co koncepcyjnie jest pojedynczym procesem przesyłania formularza.
- Ręczne zarządzanie stanem: Deweloper jest odpowiedzialny za ręczne ustawianie i resetowanie stanów ładowania i błędu w odpowiedniej kolejności w bloku try...catch...finally. Jest to powtarzalne i podatne na błędy.
- Ścisłe powiązanie: Logika obsługi wyniku przesyłania formularza jest ściśle powiązana z logiką renderowania komponentu.
Wprowadzenie useActionState: Zmiana paradygmatu
useActionState to hook Reacta zaprojektowany specjalnie do zarządzania stanem operacji asynchronicznej, takiej jak przesyłanie formularza. Usprawnia on cały proces, łącząc stan bezpośrednio z wynikiem funkcji akcji.
Jego sygnatura jest jasna i zwięzła:
const [state, formAction] = useActionState(actionFn, initialState);
Rozłóżmy go na części:
actionFn(previousState, formData)
: To Twoja funkcja asynchroniczna, która wykonuje pracę (np. wywołuje API). Otrzymuje ona poprzedni stan i dane formularza jako argumenty. Co kluczowe, to, co ta funkcja zwraca, staje się nowym stanem.initialState
: To jest wartość stanu, zanim akcja zostanie wykonana po raz pierwszy.state
: To jest bieżący stan. Początkowo przechowuje initialState i jest aktualizowany do wartości zwrotnej Twojej funkcji actionFn po każdym wykonaniu.formAction
: To nowa, opakowana wersja Twojej funkcji akcji. Powinieneś przekazać tę funkcję do propaaction
elementu<form>
. React używa tej opakowanej funkcji do śledzenia stanu oczekiwania (pending) akcji.
Praktyczny przykład: Refaktoryzacja z useActionState
Teraz zrefaktoryzujmy nasz formularz produktu przy użyciu useActionState. Poprawa jest natychmiast widoczna.
Najpierw musimy dostosować naszą logikę akcji. Zamiast rzucać błędy, akcja powinna zwracać obiekt stanu, który opisuje wynik.
Przykład: 'Nowy sposób' z useActionState
// Funkcja akcji, zaprojektowana do pracy z useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // Symulacja opóźnienia sieciowego
if (!productName || productName.length < 3) {
return { message: 'Nazwa produktu musi mieć co najmniej 3 znaki.', success: false };
}
console.log(`Produkt "${productName}" dodany.`);
// Po sukcesie zwróć komunikat i wyczyść formularz.
return { message: `Pomyślnie dodano "${productName}"`, success: true };
};
// Zrefaktoryzowany komponent
{state.message} {state.message}import { useActionState } from 'react';
// Uwaga: W następnej sekcji dodamy useFormStatus do obsługi stanu oczekiwania.
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
Spójrz, o ile to jest czystsze! Zastąpiliśmy trzy hooki useState jednym hookiem useActionState. Odpowiedzialnością komponentu jest teraz wyłącznie renderowanie interfejsu użytkownika na podstawie obiektu `state`. Cała logika biznesowa jest zgrabnie zamknięta w funkcji `addProductAction`. Stan aktualizuje się automatycznie na podstawie tego, co zwraca akcja.
Ale zaraz, co ze stanem oczekiwania? Jak wyłączyć przycisk, gdy formularz jest przesyłany?
Obsługa stanów oczekiwania za pomocą useFormStatus
React dostarcza towarzyszącego hooka, useFormStatus, zaprojektowanego do rozwiązania dokładnie tego problemu. Dostarcza on informacji o statusie ostatniego przesyłania formularza, ale z kluczową zasadą: musi być wywołany z komponentu, który jest renderowany wewnątrz <form>
, którego status chcesz śledzić.
To zachęca do czystego podziału odpowiedzialności. Tworzysz komponent specjalnie dla elementów interfejsu, które muszą być świadome statusu przesyłania formularza, jak przycisk do przesyłania.
Hook useFormStatus zwraca obiekt z kilkoma właściwościami, z których najważniejszą jest `pending`.
const { pending, data, method, action } = useFormStatus();
pending
: Wartość logiczna, która jest `true`, jeśli formularz nadrzędny jest aktualnie przesyłany, a `false` w przeciwnym razie.data
: Obiekt `FormData` zawierający przesyłane dane.method
: Ciąg znaków wskazujący metodę HTTP (`'get'` lub `'post'`).action
: Odniesienie do funkcji przekazanej do propa `action` formularza.
Tworzenie przycisku przesyłania świadomego statusu
Stwórzmy dedykowany komponent `SubmitButton` i zintegrujmy go z naszym formularzem.
Przykład: Komponent SubmitButton
import { useFormStatus } from 'react-dom';
// Uwaga: useFormStatus jest importowane z 'react-dom', a nie z 'react'.
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'Dodawanie...' : 'Dodaj produkt'}
);
}
Teraz możemy zaktualizować nasz główny komponent formularza, aby go użyć.
Przykład: Kompletny formularz z useActionState i useFormStatus
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (funkcja addProductAction pozostaje bez zmian)
function SubmitButton() { /* ... jak zdefiniowano powyżej ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* Możemy dodać klucz 'key', aby zresetować pole po sukcesie */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
Dzięki tej strukturze komponent `CompleteProductForm` nie musi nic wiedzieć o stanie oczekiwania. `SubmitButton` jest całkowicie samowystarczalny. Ten kompozycyjny wzorzec jest niezwykle potężny do budowania złożonych, łatwych w utrzymaniu interfejsów użytkownika.
Moc progresywnego ulepszania (Progressive Enhancement)
Jedną z najgłębszych korzyści tego nowego podejścia opartego na akcjach, zwłaszcza w połączeniu z Akcjami Serwerowymi (Server Actions), jest automatyczne progresywne ulepszanie. Jest to kluczowa koncepcja budowania aplikacji dla globalnej publiczności, gdzie warunki sieciowe mogą być niestabilne, a użytkownicy mogą mieć starsze urządzenia lub wyłączony JavaScript.
Oto jak to działa:
- Bez JavaScriptu: Jeśli przeglądarka użytkownika nie wykonuje JavaScriptu po stronie klienta,
<form action={...}>
działa jak standardowy formularz HTML. Wykonuje pełne przeładowanie strony z żądaniem do serwera. Jeśli używasz frameworka takiego jak Next.js, akcja po stronie serwera jest uruchamiana, a framework renderuje całą stronę na nowo z nowym stanem (np. pokazując błąd walidacji). Aplikacja jest w pełni funkcjonalna, tylko bez płynności charakterystycznej dla SPA. - Z JavaScriptem: Gdy pakiet JavaScript zostanie załadowany i React nawodni stronę, ta sama funkcja `formAction` jest wykonywana po stronie klienta. Zamiast pełnego przeładowania strony, zachowuje się jak typowe żądanie fetch. Akcja jest wywoływana, stan jest aktualizowany i tylko niezbędne części komponentu są ponownie renderowane.
Oznacza to, że piszesz logikę formularza raz, a działa ona bezproblemowo w obu scenariuszach. Domyślnie budujesz odporną i dostępną aplikację, co jest ogromną wygraną dla doświadczenia użytkownika na całym świecie.
Zaawansowane wzorce i przypadki użycia
1. Akcje serwerowe kontra akcje klienckie
Funkcja `actionFn`, którą przekazujesz do useActionState, może być standardową funkcją asynchroniczną po stronie klienta (jak w naszych przykładach) lub Akcją Serwerową. Akcja Serwerowa to funkcja zdefiniowana na serwerze, którą można wywoływać bezpośrednio z komponentów klienckich. W frameworkach takich jak Next.js, definiuje się ją, dodając dyrektywę "use server";
na początku ciała funkcji.
- Akcje klienckie: Idealne do mutacji, które wpływają tylko na stan po stronie klienta lub wywołują API firm trzecich bezpośrednio z klienta.
- Akcje serwerowe: Doskonałe do mutacji, które obejmują bazę danych lub inne zasoby po stronie serwera. Upraszczają architekturę, eliminując potrzebę ręcznego tworzenia punktów końcowych API dla każdej mutacji.
Piękno polega na tym, że useActionState działa identycznie z oboma typami akcji. Możesz zamienić akcję kliencką na serwerową bez zmiany kodu komponentu.
2. Optymistyczne aktualizacje z `useOptimistic`
Aby uzyskać jeszcze bardziej responsywne odczucia, można połączyć useActionState z hookiem useOptimistic. Optymistyczna aktualizacja polega na natychmiastowej aktualizacji interfejsu użytkownika, *zakładając*, że operacja asynchroniczna zakończy się sukcesem. Jeśli się nie powiedzie, przywracasz interfejs do poprzedniego stanu.
Wyobraź sobie aplikację społecznościową, w której dodajesz komentarz. Optymistycznie pokazałbyś nowy komentarz na liście natychmiast, podczas gdy żądanie jest wysyłane na serwer. useOptimistic jest zaprojektowany do współpracy z akcjami, aby ten wzorzec był prosty do zaimplementowania.
3. Resetowanie formularza po sukcesie
Częstym wymogiem jest czyszczenie pól formularza po udanym przesłaniu. Istnieje kilka sposobów, aby to osiągnąć z useActionState.
- Sztuczka z propem 'key': Jak pokazano w naszym przykładzie `CompleteProductForm`, można przypisać unikalny `key` do pola input lub całego formularza. Gdy klucz się zmienia, React odmontowuje stary komponent i montuje nowy, skutecznie resetując jego stan. Powiązanie klucza z flagą sukcesu (`key={state.success ? 'success' : 'initial'}`) jest prostą i skuteczną metodą.
- Komponenty kontrolowane: W razie potrzeby nadal można używać komponentów kontrolowanych. Zarządzając wartością pola za pomocą useState, można wywołać funkcję ustawiającą, aby ją wyczyścić wewnątrz useEffect, który nasłuchuje na stan sukcesu z useActionState.
Częste pułapki i dobre praktyki
- Umiejscowienie
useFormStatus
: Pamiętaj, że komponent wywołujący useFormStatus musi być renderowany jako dziecko<form>
. Nie zadziała, jeśli będzie rodzeństwem lub rodzicem. - Stan serializowalny: Podczas korzystania z Akcji Serwerowych, obiekt stanu zwracany z Twojej akcji musi być serializowalny. Oznacza to, że nie może zawierać funkcji, Symboli ani innych wartości nieserializowalnych. Trzymaj się prostych obiektów, tablic, ciągów znaków, liczb i wartości logicznych.
- Nie rzucaj błędów w akcjach: Zamiast `throw new Error()`, Twoja funkcja akcji powinna elegancko obsługiwać błędy i zwracać obiekt stanu, który opisuje błąd (np. `{ success: false, message: 'Wystąpił błąd' }`). Zapewnia to, że stan jest zawsze aktualizowany w przewidywalny sposób.
- Zdefiniuj jasny kształt stanu: Ustal spójną strukturę dla swojego obiektu stanu od samego początku. Kształt taki jak `{ data: T | null, message: string | null, success: boolean, errors: Record
| null }` może pokryć wiele przypadków użycia.
useActionState kontra useReducer: Szybkie porównanie
Na pierwszy rzut oka useActionState może wydawać się podobny do useReducer, ponieważ oba obejmują aktualizację stanu na podstawie poprzedniego stanu. Służą jednak do odrębnych celów.
useReducer
to uniwersalny hook do zarządzania złożonymi przejściami stanu po stronie klienta. Jest wyzwalany przez wysyłanie akcji i jest idealny do logiki stanu, która ma wiele możliwych, synchronicznych zmian stanu (np. złożony kreator wieloetapowy).useActionState
to wyspecjalizowany hook przeznaczony do stanu, który zmienia się w odpowiedzi na pojedynczą, zazwyczaj asynchroniczną akcję. Jego główną rolą jest integracja z formularzami HTML, Akcjami Serwerowymi i funkcjami renderowania współbieżnego Reacta, takimi jak przejścia stanu oczekiwania.
Wniosek: Do przesyłania formularzy i operacji asynchronicznych powiązanych z formularzami, useActionState jest nowoczesnym, specjalnie do tego celu stworzonym narzędziem. Do innych złożonych maszyn stanów po stronie klienta, useReducer pozostaje doskonałym wyborem.
Podsumowanie: Przyjęcie przyszłości formularzy w React
Hook useActionState to coś więcej niż tylko nowe API; reprezentuje on fundamentalną zmianę w kierunku bardziej solidnego, deklaratywnego i zorientowanego na użytkownika sposobu obsługi formularzy i mutacji danych w React. Przyjmując go, zyskujesz:
- Mniej kodu szablonowego: Pojedynczy hook zastępuje wielokrotne wywołania useState i ręczną orkiestrację stanu.
- Zintegrowane stany oczekiwania: Bezproblemowo obsługuj interfejsy ładowania za pomocą towarzyszącego hooka useFormStatus.
- Wbudowane progresywne ulepszanie: Pisz kod, który działa z JavaScriptem lub bez, zapewniając dostępność i odporność dla wszystkich użytkowników.
- Uproszczona komunikacja z serwerem: Naturalne dopasowanie do Akcji Serwerowych, usprawniające doświadczenie rozwoju full-stack.
Rozpoczynając nowe projekty lub refaktoryzując istniejące, rozważ sięgnięcie po useActionState. Nie tylko poprawi to Twoje doświadczenie deweloperskie, czyniąc kod czystszym i bardziej przewidywalnym, ale także umożliwi Ci budowanie aplikacji wyższej jakości, które są szybsze, bardziej odporne i dostępne dla zróżnicowanej globalnej publiczności.