Czym są Actions
Action to funkcja wykonująca zmianę danych, często asynchroniczna. Funkcję przekazaną do action elementu <form> React uruchamia w Transition z obiektem FormData; po jej sukcesie resetuje niekontrolowane pola. Stan pending jest dostępny przez hooki, ale to Twój kod decyduje, czy np. wyłączyć przycisk submit.
Kliencka funkcja formularza nie jest tym samym co Server Function. Server Functions (często nazywane Server Actions w dokumentacji Next.js) wykonują kod na serwerze i wymagają wsparcia frameworka oraz dyrektywy 'use server'; klientskie Actions mogą wywołać zwykłe .
Najprostszy formularz z Actions
Nie ma tu useState, preventDefault ani onSubmit. React wywoła akcję i po jej powodzeniu wyczyści niekontrolowane pola. Ten minimalny przykład nie wyłącza jednak przycisku w trakcie żądania; do tego służą isPending z useActionState albo pending z useFormStatus.
useActionState — jeden hook zamiast trzech stanów
W praktyce samo wpisanie akcji w form action nie wystarczy — trzeba obsłużyć błędy i sukces. Do tego służy useActionState.
Hook zwraca trzy rzeczy:
state— ostatni wynik zwrócony przez akcję (przy pierwszym renderze to wartość inicjalna, tu{}),formAction— opakowany handler gotowy do wpisania w<form action={...}>,isPending— flagatrue/falseinformująca, czy akcja jest w toku.
Dla porównania, ten sam formularz przed React 19 wymagał trzech osobnych wywołań useState (loading, error, success), ręcznego e.preventDefault() i obsługi błędów.
useFormStatus — stan pending w komponentach dzieci
Jeśli przycisk submit to osobny komponent, nie ma bezpośredniego dostępu do isPending z useActionState. useFormStatus rozwiązuje ten problem — odczytuje stan formularza-rodzica bez przekazywania propsów.
Hook useFormStatus działa tylko wewnątrz komponentów renderowanych jako dzieci <form> i automatycznie szuka najbliższego formularza-przodka w drzewie React.
Poza pending zwraca też data (wysyłany FormData), method i action (referencja do funkcji akcji). Gdy do action lub formAction przekazujesz funkcję, React wysyła formularz metodą POST, niezależnie od atrybutu method.
useOptimistic — natychmiastowy feedback bez czekania na serwer
pokazuje zmianę w interfejsie w
chwili kliknięcia, jeszcze zanim serwer odpowie. Optymistyczny stan istnieje w
trakcie akcji; po jej zakończeniu widok wynika ze stanu bazowego przekazanego do
useOptimistic.
Klasyczny przykład to lista komentarzy:
Użytkownik klika "Dodaj komentarz" → addOptimistic natychmiast dopisuje komentarz oczekujący → w tle idzie żądanie do API. Po sukcesie setComments aktualizuje potwierdzony stan bazowy danymi z serwera. Z kolei, jeśli akcja rzuci błąd i stan bazowy się nie zmieni, to po zakończeniu akcji tymczasowy wpis znika; na produkcji warto dodać do tego komunikat błędu lub Error Boundary.
Takie rozwiązanie zmniejsza odczuwalny czas oczekiwania, ponieważ użytkownik natychmiast widzi wpis oczekujący na potwierdzenie.
Warto rozwiać częste nieporozumienie: useOptimistic nie ma osobnego mechanizmu „rollbacku przy błędzie". Optymistyczny widok znika, bo po zakończeniu akcji React renderuje stan bazowy — a ten zmieniasz (setComments, setState) tylko po sukcesie. Gdy akcja zawiedzie i stanu bazowego nie ruszysz, interfejs po prostu wraca do tego, co pokazywał wcześniej. Nie ma więc co liczyć na automatyczne „cofnięcie" — sam decydujesz, kiedy zatwierdzić zmianę, a komunikat o błędzie dorzucasz osobno (toast albo Error Boundary).
Wariant z Server Actions i revalidatePath
W Next.js z App Routerem ten sam wzorzec łączy się z : zamiast klienckiego fetch akcja oznaczona 'use server' mutuje dane i woła revalidatePath, a odświeżony stan z serwera podmienia element tymczasowy:
Po stronie komponentu wołasz addOptimistic(text) natychmiast, a potem await addTodo(formData) — gdy revalidatePath przyniesie świeże dane, tymczasowy wpis z id: 'temp-...' zostaje zastąpiony rekordem z bazy. Szerzej o samej warstwie serwerowej piszę w artykule o Route Handlers vs Server Actions, a o jej testowaniu w testowaniu Server Actions.
Kiedy NIE używać Optimistic UI
Optimistic UI sprawdza się tam, gdzie operacja jest odwracalna, a błąd mało prawdopodobny: lajki, komentarze, dodawanie i usuwanie elementów listy, toggle. Trzymaj go natomiast z dala od:
- płatności i transakcji finansowych — użytkownik musi zobaczyć realne potwierdzenie serwera, nie optymistyczną obietnicę;
- operacji nieodwracalnych — usunięcie konta, wysłanie zamówienia;
- danych wymagających walidacji serwerowej — unikalne slugi, sprawdzanie dostępności;
- operacji długo przetwarzanych — generowanie raportu, przetwarzanie pliku.
W tych przypadkach pokaż realny isPending z loading state zamiast udawać, że już się udało.
useTransition dla akcji poza formularzem
Nie każda akcja jest wyzwalana przez <form>. Przycisk "Lubię to" czy usuwanie elementu z listy to typowe przykłady akcji bez formularza. Tu sięgasz po useTransition.
W React 19 asynchroniczna funkcja uruchamiana w startTransition jest określana jako Action. useTransition zapewnia tu isPending, ale nie dodaje semantyki formularza: nie przekazuje FormData, nie resetuje pól i nie zapewnia Progressive Enhancement.
Walidacja z Zod i useActionState
Zod dobrze współpracuje z useActionState, bo pozwala zwrócić błędy pól w stanie formularza. Poniższy przykład waliduje dane przed wysłaniem do API; ten sam schemat należy zastosować również w endpointcie lub Server Function, bo walidacja klienta nie chroni serwera.
z.safeParse nie rzuca wyjątków — zwraca obiekt z polem success i, w przypadku błędu, error. Błędy walidacji lądują w stanie formularza, a nie w Error Boundary. W Next.js walidację umieść w Server Function; przy klasycznym API powtórz ją w endpointcie.
Więcej o łączeniu walidacji z React Hook Form i Zodem opisuję w osobnym artykule o React Hook Form i Zod w Next.js.
Progressive Enhancement
dla funkcji
przekazywanych do action działa z Server Functions oznaczonymi 'use server'.
W takim przypadku framework umożliwia przesłanie danych formularza do serwera
bez potrzeby załadowania JavaScriptu. Formularz może działać przed hydracją albo
przy wyłączonym JS, o ile przeglądarka ma połączenie z serwerem.
Kliencka funkcja w action w aplikacji (np. Vite) nie wykona się bez JavaScriptu. Jeśli zależy Ci na PE z React Server Functions, potrzebujesz Next.js z App Routerem lub innego frameworka wspierającego tę integrację.
Typowe pułapki
Kilka rzeczy, które regularnie powodują problemy przy pierwszym kontakcie z Actions:
useActionStatedostaje poprzedni stan jako pierwszy argument — sygnatura funkcji akcji to(prevState, formData), nie(formData). PominięcieprevStateskutkuje błędem typowania albo nieprawidłowym zachowaniem przy kolejnych wywołaniach.useOptimisticdziała tylko wewnątrz akcji lubstartTransition— próba wywołaniaaddOptimisticpoza tymi kontekstami rzuca ostrzeżenie w trybie dev.- Nie rzucaj wyjątków z akcji do walidacji — błąd zamiast
throw new Error(...)powinien wychodzić jako zwrócony obiekt{ error: '...' }. Wyjątki trafiają do Error Boundary i rozbijają drzewo komponentów. - Upload plików to osobny problem — Actions nie mają API do śledzenia postępu przesyłania. Do uploaderów z procentowym paskiem postępu użyj np.
XMLHttpRequestalbo biblioteki oferującej upload progress.
Kiedy nie używać Actions
Actions działają świetnie przy operacjach zorientowanych na serwer. Są niewłaściwym wyborem w trzech sytuacjach:
- Duże formularze wieloetapowe z dziesiątkami pól, walidacją na żywo i logiką warunkową między sekcjami. Tu React Hook Form z Zodem daje precyzyjniejszą kontrolę nad stanem po stronie klienta.
- Podgląd na żywo (np. edytor Markdown z preview obok) — każda zmiana w polu nie powinna wyzwalać akcji serwera. Klasyczny
useStatez debouncingiem jest tu właściwym narzędziem. - Przesyłanie plików z paskiem postępu — brak natywnego API do odczytu procentowego postępu przesyłania.
Migracja z onSubmit
Jeśli masz istniejący projekt z formularzami opartymi na onSubmit + useState, warto zacząć migrację od najprostszych przypadków:
- Nowe formularze — od razu pisz z Actions.
- Formularze z React Hook Form — zostaw je bez zmian, chyba że chcesz uprościć konkretny komponent.
- Proste formularze z
onSubmit + useState— to najlepsi kandydaci do migracji, bo zwykle pozwalają usunąć ręczną obsługę stanu pending i submitu.
useActionState nie jest zamiennikiem dla każdego useState — to narzędzie do konkretnego wzorca: asynchroniczna akcja z wynikiem, stanem pending i obsługą błędów.
