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 API.
Najprostszy formularz z Actions
Code
function ContactForm() { async function sendMessage(formData: FormData) { const email = formData.get('email') as string const message = formData.get('message') as string const response = await fetch('/api/contact', { method: 'POST', body: JSON.stringify({ email, message }), }) if (!response.ok) { throw new Error('Nie udało się wysłać formularza.') } } return ( <form action={sendMessage}> <input name="email" type="email" required /> <textarea name="message" required /> <button type="submit">Wyślij</button> </form> )}
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.
state — ostatni wynik zwrócony przez akcję (przy pierwszym renderze to wartość inicjalna, tu {}),
formAction — opakowany handler gotowy do wpisania w <form action={...}>,
isPending — flaga true/false informują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
Optimistic UI to wzorzec, w którym interfejs natychmiast pokazuje wynik akcji użytkownika, zakładając jej powodzenie, i wycofuje zmianę dopiero jeśli serwer zwróci błąd. 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.
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.
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.
Progressive Enhancement — podejście do budowania funkcji tak, aby działały poprawnie nawet bez JavaScriptu w przeglądarce. 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 CSR (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:
useActionState dostaje poprzedni stan jako pierwszy argument — sygnatura funkcji akcji to (prevState, formData), nie (formData). Pominięcie prevState skutkuje błędem typowania albo nieprawidłowym zachowaniem przy kolejnych wywołaniach.
useOptimistic działa tylko wewnątrz akcji lub startTransition — próba wywołania addOptimistic poza 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. XMLHttpRequest albo biblioteki oferującej upload progress.
Kiedy nie używać Actions
Actions działają świetnie przy operacjach CRUD 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 useState z 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.
— praktyka React 19
Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.