useFormStatus w React 19 — koniec z props drillingiem w formularzach

useFormStatus hook w React 19 — dostęp do stanu formularza bez props drillingu

Hook useFormStatus pojawił się w React 19 jako odpowiedź na problem, który zostaje nierozwiązany nawet gdy używacie useActionState — co z komponentami zagnieżdżonymi wewnątrz formularza, które też chcą wiedzieć czy akcja jest w toku?

poprzednim wpisie pokazywałam jak useActionState React 19 eliminuje ręczne zarządzanie pending state i błędami. Na końcu wspomniałam o jednym problemie, który mimo wszystko zostaje — co jeśli przycisk submit jest osobnym komponentem, trzy poziomy głębiej w drzewie? Props drilling zaczyna boleć. 📈

useFormStatus rozwiązuje dokładnie to. I jest zaskakująco prosty. 🙂

😬 Problem — props drilling dla isPending

Weźmy formularz z poprzedniego wpisu, ale tym razem wydzielmy przycisk submit do osobnego komponentu — co w realnych aplikacjach zdarza się bardzo często (design systemy, biblioteki komponentów, współdzielone UI).

// Osobny komponent przycisku — np. z design systemu
function SubmitButton({ isPending }: { isPending: boolean }) {
  return (
    <button type="submit" disabled={isPending}>
      {isPending ? 'Zapisywanie...' : 'Zapisz'}
    </button>
  );
}

function UpdateNameForm() {
  const [state, action, isPending] = useActionState(updateNameAction, {
    error: null,
    success: false,
  });

  return (
    <form action={action}>
      <input id="name" name="name" disabled={isPending} />
      {state.error && <p className="error">{state.error}</p>}
      {/* Musicie przekazać isPending przez props */}
      <SubmitButton isPending={isPending} />
    </form>
  );
}

Na tym poziomie wygląda niewinnie. Ale wyobraźcie sobie że SubmitButton jest trzy poziomy głębiej w drzewie komponentów, albo że isPending musicie przekazać do kilku różnych elementów naraz — inputów, przycisków, nakładki ładowania. Props drilling zaczyna boleć. 📈

✨ Rozwiązanie — useFormStatus

useFormStatus to hook, który odczytuje stan formularza z kontekstu — bez żadnych propsów. Działa jak useContext, ale specjalnie dla formularzy.

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Zapisywanie...' : 'Zapisz'}
    </button>
  );
}

function UpdateNameForm() {
  const [state, action] = useActionState(updateNameAction, {
    error: null,
    success: false,
  });

  return (
    <form action={action}>
      <input id="name" name="name" />
      {state.error && <p className="error">{state.error}</p>}
      {/* Żadnych propsów! */}
      <SubmitButton />
    </form>
  );
}

SubmitButton sam wie czy formularz jest w toku — bez żadnych propsów przekazywanych z zewnątrz.

⚠️ Ważne: useFormStatus importujemy z react-dom, nie z react — odwrotnie niż useActionState. Łatwo się pomylić. 👀

🔍 Co zwraca useFormStatus?

Hook zwraca obiekt z czterema właściwościami:

const { pending, data, method, action } = useFormStatus();
  • pending — true gdy formularz jest w trakcie wysyłania. To najczęściej używana właściwość. ⏳
  • data — obiekt FormData z danymi które są aktualnie wysyłane. Przydatny gdy chcecie np. pokazać podgląd wpisanej wartości podczas ładowania.
  • method — metoda HTTP formularza (get lub post).
  • action — referencja do funkcji akcji przekazanej do formularza.

W praktyce 90% przypadków to samo pending. Ale data potrafi być bardzo użyteczne — zaraz pokażę przykład. 😊

💡 Praktyczny przykład — przycisk z podglądem

Wyobraźcie sobie formularz gdzie użytkownik wpisuje nową nazwę i klika “Zapisz”. Zamiast pokazywać generyczny “Zapisywanie…”, możecie pokazać dokładnie co jest zapisywane — dzięki data:

function SubmitButton() {
  const { pending, data } = useFormStatus();

  const newName = data?.get('name') as string | null;

  return (
    <button type="submit" disabled={pending}>
      {pending && newName
        ? `Zapisywanie "${newName}"...`
        : 'Zapisz'}
    </button>
  );
}

Użytkownik widzi Zapisywanie "Iwona"... zamiast generycznego spinnera. Mały detal, ale robi różnicę w odczuciu responsywności UI. 🎯

🌳 Ważna zasada — komponent musi być wewnątrz <form>

useFormStatus czyta stan z najbliższego formularza wyżej w drzewie komponentów. Jeśli użyjecie go poza formularzem — pending będzie zawsze false.

// ✅ Działa — SubmitButton jest wewnątrz <form>
function UpdateNameForm() {
  return (
    <form action={action}>
      <SubmitButton /> {/* ma dostęp do stanu formularza */}
    </form>
  );
}

// ❌ Nie działa — useFormStatus użyty w tym samym komponencie co <form>
function UpdateNameForm() {
  const { pending } = useFormStatus(); // zawsze false!

  return (
    <form action={action}>
      <button disabled={pending}>Zapisz</button>
    </form>
  );
}

Ta druga wersja to częsty błąd — hook musi być w potomku formularza, nie w tym samym komponencie. Jeśli chcecie isPending w komponencie który renderuje <form>, użyjcie trzeciego elementu z useActionState. 😊

🔄 useFormStatus + useActionState razem

W praktyce oba hooki świetnie się uzupełniają. useActionState zarządza stanem akcji (błędy, sukces, dane wynikowe), a useFormStatus udostępnia stan pending głębiej w drzewie bez propsów:

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

// Komponenty potomne korzystają z useFormStatus
function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Zapisywanie...' : 'Zapisz'}
    </button>
  );
}

function NameInput() {
  const { pending } = useFormStatus();
  return (
    <input
      id="name"
      name="name"
      disabled={pending}
      placeholder="Wpisz nową nazwę"
    />
  );
}

// Formularz korzysta z useActionState
function UpdateNameForm() {
  const [state, action] = useActionState(updateNameAction, {
    error: null,
    success: false,
  });

  return (
    <form action={action}>
      <label htmlFor="name">Nowa nazwa</label>
      <NameInput />
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">Nazwa została zmieniona!</p>}
      <SubmitButton />
    </form>
  );
}

Zauważcie że UpdateNameForm nie przekazuje żadnych propsów do NameInput ani SubmitButton — a mimo to oba wiedzą kiedy formularz jest w toku. 💪

⚠️ Na co uważać

Import z react-dom, nie z react 👀

// ✅
import { useFormStatus } from 'react-dom';

// ❌
import { useFormStatus } from 'react';

Hook musi być w potomku <form>, nie w tym samym komponencie 🌳

Jeśli pending zawsze zwraca false — to prawdopodobnie właśnie ten problem.

data jest dostępne tylko podczas wysyłania 📋

Poza stanem pending data zwraca null. Zawsze sprawdzajcie pending && data przed odczytem wartości.

🤷 Kiedy useFormStatus nie jest potrzebny?

Jeśli przycisk submit i formularz są w tym samym komponencie — używajcie po prostu isPending z useActionStateuseFormStatus ma sens dopiero gdy:

  • 🌳 komponent przycisku jest wydzielony (design system, biblioteka)
  • 🌳 kilka różnych komponentów potomnych musi reagować na stan formularza
  • 🌳 chcecie unikać przekazywania isPending przez wiele poziomów propsów

📋 Podsumowanie

React 18React 19 + useFormStatus
⏳ Pending w potomkuProps drillinguseFormStatus()
📋 Dane formularza w potomkuProps drillingdata z useFormStatus()
🧩 Izolacja komponentówTrudna — zależność od propsówŁatwa — hook czyta z kontekstu

useFormStatus to mały hook, ale rozwiązuje konkretny problem który pojawia się w każdej większej aplikacji — jak przekazać stan formularza do zagnieżdżonych komponentów bez zaśmiecania ich interfejsu propsami.

W kolejnym wpisie zajmę się useOptimistic — hookiem, który pozwala aktualizować UI natychmiast, jeszcze zanim serwer odpowie. Jeśli chcieliście kiedyś żeby Wasza aplikacja reagowała błyskawicznie na akcje użytkownika — to będzie post dla Was. 😊

Używacie już komponentów z design systemu w formularzach? Ciekawa jestem jak radzicie sobie z przekazywaniem stanu pending — czy props drilling Was boli tak samo jak mnie. 👇

You might also like