useFormStatus в React 19: как получить pending из дочерних компонентов без prop drilling. Примеры, подводные камни и практические советы. Читайте и применяйте!
Если вы когда-нибудь передавали проп isPending через три уровня компонентов, чтобы просто задизейблить кнопку, — эта статья для вас. В React 19 появился хук useFormStatus, который решает эту проблему элегантно. Но у него есть одно важное правило, нарушив которое, вы потратите десять минут на поиск ошибки. Давайте разберёмся, как работает этот хук и где его действительно стоит применять.
Представьте форму заказа, разбитую на несколько компонентов: поля оплаты в одном файле, кнопка — в другом. Оба должны знать, идёт ли отправка прямо сейчас. Без useFormStatus вам придётся прокидывать isPending от родителя к каждому ребёнку.
import { useActionState } from "react";
function PaymentFields({ disabled }) {
return (
<fieldset disabled={disabled}>
<input name="cardNumber" placeholder="Card number" />
<input name="cardExpiry" placeholder="MM/YY" />
</fieldset>
);
}
function PlaceOrderButton({ pending }) {
return (
<button type="submit" disabled={pending}>
{pending ? "Placing order..." : "Place order"}
</button>
);
}
function OrderForm() {
const [state, formAction, isPending] = useActionState(placeOrder, { message: "" });
return (
<form action={formAction}>
<PaymentFields disabled={isPending} />
<PlaceOrderButton pending={isPending} />
</form>
);
}Это работает, но каждый новый компонент (секция доставки, купон, сайдбар) требует ещё один проп. useFormStatus создан, чтобы избавиться от этой рутины.
Моя первая попытка — вызвать useFormStatus прямо в OrderForm, как замену isPending. Логично же?
import { useFormStatus } from "react-dom";
function OrderForm() {
const { pending } = useFormStatus(); // всегда false
return (
<form action={placeOrder}>
<PaymentFields disabled={pending} />
<PlaceOrderButton pending={pending} />
</form>
);
}Нажимаю «Place order» — ничего не происходит. Кнопка активна, сеть показывает запрос. Ни предупреждений, ни ошибок. Я потратил несколько минут, проверяя асинхронность placeOrder, прежде чем понял: дело в самом хуке.
Правило: компонент, вызывающий useFormStatus, должен быть потомком формы, а не тем, кто её рендерит. Когда OrderForm вызывает хук, он ещё не создал <form> — тот находится в процессе рендеринга. Для хука нет родительской формы, поэтому он возвращает состояние по умолчанию: pending: false.
Исправление простое: переместите вызов useFormStatus в те компоненты, которые находятся внутри <form>.
import { useFormStatus } from "react-dom";
function PaymentFields() {
const { pending } = useFormStatus();
return (
<fieldset disabled={pending}>
<input name="cardNumber" placeholder="Card number" />
<input name="cardExpiry" placeholder="MM/YY" />
</fieldset>
);
}
function PlaceOrderButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Placing order..." : "Place order"}
</button>
);
}
function OrderForm() {
return (
<form action={placeOrder}>
<PaymentFields />
<PlaceOrderButton />
</form>
);
}Теперь pending обновляется корректно. Никаких пропов — каждый ребёнок сам спрашивает у родительской формы её статус.
Не перепутайте импорт: useFormStatus находится в react-dom, а не в react. Если вы, как и я, привыкли к useActionState из react, легко ошибиться. Ошибка не вызовет предупреждение, просто всё перестанет работать.
Когда pending заработал, я думал, что это всё. Но хук возвращает объект с четырьмя свойствами:
const { pending, data, method, action } = useFormStatus();FormData текущей отправки, доступен только пока идёт запрос. Я использовал его, чтобы сделать текст кнопки конкретнее:function PlaceOrderButton() {
const { pending, data } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? `Placing order for ${data.get("email")}...` : "Place order"}
</button>
);
}Обратите внимание: data равен null, когда отправка не идёт. Поэтому data.get("email") безопасен только внутри ветки pending. Если вынести его наружу — компонент упадёт при первом рендере.
'post', независимо от атрибута method. 'get' появится только если action — обычная строка URL или его нет вовсе.action формы. Если функции нет (или формы нет) — null. Проверка на null не различает эти два случая.Мой компонент PlaceOrderButton случайно оказался переиспользован на другой форме — «Сохранить черновик заказа». Я ожидал, что понадобится контекст или ID, чтобы различить формы. Ничего не нужно! useFormStatus сам находит ближайшую родительскую форму в каждом случае. Тот же компонент корректно читал статус на обеих формах без единой строчки дополнительного кода.
А вот что меня удивило: в админке кнопка была вынесена за пределы формы с помощью HTML-атрибута form="orderForm". Это валидный HTML, и отправка работает. Но useFormStatus внутри такой кнопки всегда возвращает pending: false, даже когда форма отправляется. Хук работает с деревом компонентов, а не с DOM-атрибутами. Для него эта кнопка — не потомок формы.
Для простой формы из двух полей и одной кнопки в одном компоненте useActionState с его isPending — проще и не требует второго хука. useFormStatus оправдан, когда части формы реально разнесены по компонентам. И помните: хук сообщает о статусе уже начавшейся отправки, а не валидирует её. Проверки — удел action-функции.
Теперь, когда мы прошли три части, картина ясна:
Ключевое различие: isPending из useActionState следует за конкретным действием, а pending из useFormStatus — за формой, в которой находится компонент. Поэтому один и тот же PlaceOrderButton работал на двух разных формах с разными действиями.
Эти хуки не конфликтуют. В моей итоговой форме заказа я использовал оба: useActionState в родителе для результата, useFormStatus в каждом дочернем разделе для статуса.
Если у вас есть форма, где кнопка отправки находится на три уровня глубже компонента, управляющего отправкой, — проверьте, где именно вы вызвали useFormStatus. Скорее всего, вы сделали ту же ошибку, что и я, и потеряли десять минут. А теперь примените знание: вынесите хук в дочерние компоненты, и состояние будет работать без магии.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →