Přeskočit na obsah

useActionState: formuláře bez zbytečného boilerplatu

Tři useState pro data, chybu a loading? React 19 to zvládne jedním hookem — a formulář funguje i před hydratací.

Autor
Jan Jarčík
Publikováno
Doba čtení
2 min čtení
Aktualizováno
SdíletXLinkedInBluesky

Klasický formulář v Reactu vypadal roky stejně: useState pro hodnoty, další pro chybu, další pro isLoading, onSubmit s preventDefault() a try/finally. React 19 přináší useActionState, který tenhle vzor zabalí do jediného hooku.

Jak to funguje

useActionState(action, initialState) vrací trojici:

  • state — poslední návratová hodnota akce (na začátku initialState),
  • formAction — funkci, kterou předáte do <form action={…}>,
  • isPending — true, dokud akce běží.

Akce dostane předchozí stav a FormData. Co vrátí, to se stane novým stavem.

TSX
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
 
type State = { ok: boolean; message: string } | null;
 
export function NewsletterForm() {
  const [state, formAction, isPending] = useActionState<State, FormData>(
    async (_prev, formData) => {
      const email = String(formData.get("email") ?? "");
      if (!email.includes("@")) return { ok: false, message: "Neplatný e-mail" };
      return subscribe(email);
    },
    null,
  );
 
  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button disabled={isPending}>
        {isPending ? "Odesílám…" : "Odebírat"}
      </button>
      {state && <p role="status">{state.message}</p>}
    </form>
  );
}

Proč je to lepší

  1. Žádné ručně řízené isLoading. Pending stav drží React a nikdy se „nezasekne“ na true, když zapomenete finally.
  2. Akce se řadí za sebou. Když uživatel klikne dvakrát, druhé volání dostane jako prev výsledek prvního.
  3. Progressive enhancement. Pokud je akce Server Function ("use server"), formulář se odešle i tehdy, když JavaScript ještě není načtený.

Tip na závěr

Formulář se po úspěšné akci sám resetuje (u nekontrolovaných inputů). Pokud chcete hodnoty po chybě zachovat, vraťte je ve stavu a použijte defaultValue={state?.values?.email}.