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?
W 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:
useFormStatusimportujemy zreact-dom, nie zreact— 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—truegdy formularz jest w trakcie wysyłania. To najczęściej używana właściwość. ⏳data— obiektFormDataz danymi które są aktualnie wysyłane. Przydatny gdy chcecie np. pokazać podgląd wpisanej wartości podczas ładowania.method— metoda HTTP formularza (getlubpost).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 useActionState. useFormStatus 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
isPendingprzez wiele poziomów propsów
📋 Podsumowanie
| React 18 | React 19 + useFormStatus | |
|---|---|---|
| ⏳ Pending w potomku | Props drilling | useFormStatus() |
| 📋 Dane formularza w potomku | Props drilling | data z useFormStatus() |
| 🧩 Izolacja komponentów | Trudna — 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. 👇



