StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Konsultacje

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • Audyt SEO i Performance
  • Testy automatyczne i QA
  • Konsultacje Produktowe
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
Next.jsReactUX

React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce

Koniec z onSubmit i ręcznym stanem ładowania — React 19 Actions przepisują formularze od fundamentów. Migracja bez bólu głowy.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
24 kwietnia 2026 00:00
Czytanie
6 min czytania
Aktualizacja
25 maja 2026 16:00

React 19 pozwala przekazać funkcję bezpośrednio do atrybutu action elementu <form>. Dzięki integracji z Actions, useActionState, useFormStatus i useOptimistic możesz ograniczyć ręczną obsługę onSubmit, pending state oraz optymistycznych aktualizacji, zachowując kontrolę nad błędami i stanem trwałym.

Artykuł w skrócie

  • Actions obsługują submit deklaratywnie — funkcja w form action={...} otrzymuje FormData, a po jej sukcesie React resetuje niekontrolowane pola formularza.
  • useActionState scala stan wysyłki — jeden hook zwraca wynik akcji, zmodyfikowany handler i flagę isPending. Koniec z osobnymi useState dla loading, error i success.
  • useFormStatus synchronizuje komponenty dzieci — przycisk submit może odczytać stan pending rodzica bez przekazywania propsów przez drzewo.
  • useOptimistic daje natychmiastowy feedback — UI pokazuje tymczasową zmianę w trakcie akcji, a sukces musi zaktualizować stan bazowy.
  • Actions nie zastępują React Hook Form — przy złożonych formularzach z dynamicznymi polami i walidacją na żywo, RHF nadal ma przewagę.

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 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.

Code
import { useActionState } from 'react'
 
type FormState = {
  success?: boolean
  error?: string
}
 
async function contactAction(
  prevState: FormState,
  formData: FormData,
): Promise<FormState> {
  const email = formData.get('email') as string
  const message = formData.get('message') as string
 
  if (!email || !message) {
    return { error: 'Uzupełnij wszystkie pola.' }
  }
 
  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify({ email, message }),
    })
 
    if (!response.ok) {
      return { error: 'Nie udało się wysłać wiadomości. Spróbuj ponownie.' }
    }
 
    return { success: true }
  } catch {
    return { error: 'Nie udało się wysłać wiadomości. Spróbuj ponownie.' }
  }
}
 
function ContactForm() {
  const [state, formAction, isPending] = useActionState(contactAction, {})
 
  if (state.success) {
    return <p>Wiadomość wysłana. Odezwę się wkrótce.</p>
  }
 
  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      {state.error && <p className="error">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Wysyłanie...' : 'Wyślij'}
      </button>
    </form>
  )
}

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 — 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.

Top tip

W formularzu useActionState wywołuje funkcję o kształcie (prevState, formData) => State | Promise<State>, a parametr prevState pozwala budować kumulowane stany, np. listy błędów z kolejnych wysyłek.

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.

Code
import { useFormStatus } from 'react-dom'
 
function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus()
 
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Wysyłanie...' : children}
    </button>
  )
}
 
function ContactForm() {
  return (
    <form action={sendMessage}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <SubmitButton>Wyślij wiadomość</SubmitButton>
    </form>
  )
}

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.

Klasyczny przykład to lista komentarzy:

Code
import { useOptimistic, useState } from 'react'
 
type Comment = { id: string; text: string; pending?: boolean }
 
async function addComment(formData: FormData): Promise<Comment> {
  const text = formData.get('text') as string
  const response = await fetch('/api/comments', {
    method: 'POST',
    body: JSON.stringify({ text }),
  })
 
  if (!response.ok) {
    throw new Error('Nie udało się dodać komentarza.')
  }
 
  return response.json()
}
 
function Comments({ initialComments }: { initialComments: Comment[] }) {
  const [comments, setComments] = useState(initialComments)
  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    (currentComments, newComment: Comment) => [...currentComments, newComment],
  )
 
  async function handleAction(formData: FormData) {
    const text = formData.get('text') as string
    addOptimistic({ id: crypto.randomUUID(), text, pending: true })
 
    const savedComment = await addComment(formData)
    setComments((current) => [...current, savedComment])
  }
 
  return (
    <>
      <ul>
        {optimisticComments.map((comment) => (
          <li key={comment.id}>
            {comment.text} {comment.pending && '(wysyłanie...)'}
          </li>
        ))}
      </ul>
      <form action={handleAction}>
        <input name="text" required />
        <button type="submit">Dodaj komentarz</button>
      </form>
    </>
  )
}

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.

Code
import { useState, useTransition } from 'react'
 
function LikeButton({
  postId,
  initialLikes,
}: {
  postId: string
  initialLikes: number
}) {
  const [likes, setLikes] = useState(initialLikes)
  const [isPending, startTransition] = useTransition()
 
  const handleLike = () => {
    startTransition(async () => {
      setLikes((prev) => prev + 1)
      try {
        const response = await fetch(`/api/posts/${postId}/like`, {
          method: 'POST',
        })
        if (!response.ok) {
          throw new Error('Nie udało się zapisać polubienia.')
        }
      } catch {
        setLikes((prev) => prev - 1)
      }
    })
  }
 
  return (
    <button onClick={handleLike} disabled={isPending}>
      ❤️ {likes}
    </button>
  )
}

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.

Code
import { z } from 'zod'
 
const ContactSchema = z.object({
  email: z.string().email('Podaj prawidłowy adres e-mail.'),
  message: z.string().min(10, 'Wiadomość musi mieć co najmniej 10 znaków.'),
})
 
type ContactFormState = {
  errors?: {
    email?: string[]
    message?: string[]
    _form?: string[]
  }
  success?: boolean
}
 
async function contactAction(
  _prevState: ContactFormState,
  formData: FormData,
): Promise<ContactFormState> {
  const parsed = ContactSchema.safeParse({
    email: formData.get('email'),
    message: formData.get('message'),
  })
 
  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors }
  }
 
  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify(parsed.data),
    })
 
    if (!response.ok) {
      return {
        errors: { _form: ['Błąd serwera. Spróbuj ponownie za chwilę.'] },
      }
    }
 
    return { success: true }
  } catch {
    return { errors: { _form: ['Błąd serwera. Spróbuj ponownie za chwilę.'] } }
  }
}
 
function ContactForm() {
  const [state, formAction, isPending] = useActionState(contactAction, {})
 
  if (state.success) {
    return <p>Wiadomość wysłana.</p>
  }
 
  return (
    <form action={formAction}>
      <div>
        <label htmlFor="email">E-mail</label>
        <input id="email" name="email" type="email" />
        {state.errors?.email?.map((err) => (
          <p key={err} className="error">
            {err}
          </p>
        ))}
      </div>
 
      <div>
        <label htmlFor="message">Wiadomość</label>
        <textarea id="message" name="message" />
        {state.errors?.message?.map((err) => (
          <p key={err} className="error">
            {err}
          </p>
        ))}
      </div>
 
      {state.errors?._form?.map((err) => (
        <p key={err} className="error">
          {err}
        </p>
      ))}
 
      <button type="submit" disabled={isPending}>
        {isPending ? 'Wysyłanie...' : 'Wyślij'}
      </button>
    </form>
  )
}

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 artykule React Hook Form + Zod w Next.js.

Progressive Enhancement

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ę.

Notatka

Progressive Enhancement pomaga, gdy JavaScript ładuje się wolno albo jest wyłączony. Nie sprawia, że wysłanie formularza zadziała bez połączenia sieciowego.

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:

  1. Nowe formularze — od razu pisz z Actions.
  2. Formularze z React Hook Form — zostaw je bez zmian, chyba że chcesz uprościć konkretny komponent.
  3. 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

Werdykt Labu

React 19 Actions eliminują większość boilerplate'u, który latami towarzyszył formularzom — zamiast żonglować osobnymi useState dla loading, error i success, masz useActionState zwracający [state, formAction, isPending], useFormStatus synchronizujący przyciski w głębi drzewa i useOptimistic dający natychmiastowy feedback. Funkcję przekazujesz wprost do action formularza, a React sam uruchamia ją w Transition i resetuje pola po sukcesie.

Klucz to wiedzieć, gdzie Actions są właściwym narzędziem, a gdzie nie. Przy prostych formularzach zorientowanych na serwer — newsletter, kontakt, edycja profilu — wygrywają czytelnością i progressive enhancement. Przy złożonych formularzach z walidacją na żywo, polami dynamicznymi i zagnieżdżonymi listami React Hook Form nadal ma przewagę. Błędy oczekiwane (walidacja) zwracaj jako stan, a nie wyjątek — wyjątki zostaw dla awarii i Error Boundary.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
SEO & Performance
  • Czym są Actions1 min
  • Najprostszy formularz z Actions1 min
  • useActionState — jeden hook zamiast trzech stanów1 min
  • useFormStatus — stan pending w komponentach dzieci1 min
  • useOptimistic — natychmiastowy feedback bez czekania na serwer1 min
  • useTransition dla akcji poza formularzem1 min
  • Walidacja z Zod i useActionState1 min
  • Progressive Enhancement1 min
  • Typowe pułapki1 min
  • Kiedy nie używać Actions1 min
  • Migracja z onSubmit1 min
  • Werdykt Labu1 min

Często zadawane pytania

ŹródłaZweryfikowano: 31 maja 2026

API React 19 oraz integrację z Server Actions w Next.js zweryfikowano na podstawie oficjalnej dokumentacji React i Next.js:

React 19 release notes, React docs — <form>, React docs — useActionState, React docs — useOptimistic, React docs — useFormStatus, Next.js — Updating Data.

Seria

React w praktyce 2026
Część 1 / 4
  1. React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce
  2. 2React Compiler w 2026 — czy useMemo i useCallback są już martwe?
  3. 3React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
  4. 4TypeScript w React bez bólu — 7 wzorców, które realnie robią różnicę
Maciej Sala

O autorze

Maciej Sala

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.

Moje artykułyWięcej o mnie

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Dlaczego doświadczeni programiści porzucają React na rzecz czystego JS i HTMX
Dlaczego doświadczeni programiści porzucają React na rzecz czystego JS i HTMX

Doświadczeni programiści rezygnują z React — nie z frustracji, ale z powodów technicznych. Kiedy vanilla JS i HTMX to odpowiedź, a kiedy nie?

Maciej Sala

Maciej Sala

Founder Strivelab

4 sierpnia 2025
Next.js 15 — co nowego i czy warto migrować z 14?
Next.js 15 — co nowego i czy warto migrować z 14?

Next.js 15 zmienia model cache i dodaje async params — migracja z 14 nie jest tylko aktualizacją. Co naprawdę się zmienia i czy warto?

Maciej Sala

Maciej Sala

Founder Strivelab

10 kwietnia 2026
Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków
Astro.js vs Next.js w 2026 — kompleksowe porównanie frameworków

Astro 6 vs Next.js 16 — zupełnie różne założenia. Które wybrać do strony usługowej, bloga, SaaS i e-commerce? Decydujące kryteria.

Maciej Sala

Maciej Sala

Founder Strivelab

15 kwietnia 2026
Poprzedni wpisReact Compiler w 2026 — czy useMemo i useCallback są już martwe?React Compiler jest stabilny — ale czy naprawdę możesz teraz usunąć wszystkie useMemo i useCallback? Kiedy Compiler wyręcza Cię, a kiedy nie.
Maciej Sala

Maciej Sala

Founder Strivelab

24 kwietnia 2026
Następny wpisMigracja bloga z WordPress na Astro — eksport treści, przekierowania 301 i zachowanie pozycji w GoogleMigracja bloga z WordPress na Astro bez utraty pozycji w Google — jak wyeksportować treść, zmapować URL-e i nie zepsuć 301-ek?
Maciej Sala

Maciej Sala

Founder Strivelab

24 kwietnia 2026