React для разработчиков Vue, часть 4: Формы, useFormStatus и useOptimistic

Источник: Telerik Blogs•

React для разработчиков Vue, часть 4: Формы, useFormStatus и useOptimistic

React 19 для разработчиков Vue 3, часть 4: Там, где во Vue вы используете v-model и ref или стороннюю библиотеку, React полагается на стандартный HTML <form> и хуки.

React 19 для разработчиков Vue 3, часть 4: Там, где в Vue вы используете v-model и ref или стороннюю библиотеку, React полагается на обычный HTML-тег <form> и хуки.

В третьей части этой серии мы познакомились с Actions и увидели, как форма вызывает действие через свойство action. В этот раз мы останемся в этой теме и углубимся в детали. Формы — это то место, где многое в React 19 наконец встает на свои места, так что это самая интересная часть.

В Vue у вас уже есть удобный механизм для работы с формами. Вы связываете поля ввода с помощью v-model, вручную отслеживаете состояние загрузки через ref, а для всего остального используете библиотеки вроде VeeValidate. React 19 выбирает другой путь. Он опирается на стандартный HTML-тег <form> и предоставляет несколько небольших хуков, которые избавляют от большей части ручной работы.

Ни один из этих хуков не является чем-то грандиозным сам по себе. Однако вместе они заменяют множество «склеивающего» кода, который вы обычно пишете вручную в Vue, и в этом вся суть. Давайте взглянем.

Изображение сгенерировано ИИ

Управляемые поля ввода (Controlled Inputs): ручной аналог v-model

Первое, чего не хватает каждому разработчику Vue в первый же день — это v-model. В React его нет. Не существует единого атрибута, который одновременно отображает значение и обновляет его при вводе пользователем. Вы связываете эти две части самостоятельно.

NameField.vue

NameField.jsx

Давайте разберем это. В Vue v-model выполняет две задачи одновременно: он помещает значение name в поле ввода и записывает введенные данные обратно в name при наборе текста. В React вы выполняете обе задачи вручную. value={name} отображает текущее значение, а onChange срабатывает при каждом нажатии клавиши и вызывает setName с новым текстом. Вы считываете введенное значение из event.target.value.

Поле ввода, настроенное таким образом, где React владеет значением через состояние (state), называется управляемым (controlled input). React контролирует содержимое поля, а не браузер. Таким образом, v-model="name" превращается в связку value={name} плюс onChange, и эту пару вам придется писать довольно часто.

Заметьте, что в простых случаях это требует больше кода, чем в Vue, и я не буду утверждать обратное. Преимущество проявляется позже, так как наличие значения в вашем собственном состоянии делает валидацию, форматирование и условную логику очень прямыми. У вас уже есть значение, поэтому вы просто используете его.

А как насчет формы с пятью полями? В Vue вы бы добавили пять привязок v-model и пошли дальше. Управляемый подход в React означает пять фрагментов состояния, по одному на каждое поле, каждое со своим значением и onChange. Это много лишнего кода для большой формы. Это главная причина появления следующего паттерна. Когда форма — это просто «заполнил и отправил», вам вовсе не обязательно контролировать каждый ввод.

Позвольте форме сделать всю работу

Вот часть, которая удивила меня после перехода с Vue. Вам не всегда нужны управляемые поля ввода. React 19 может считывать значения прямо из формы при отправке, как это всегда было возможно в обычном HTML.

Вспомните Actions из третьей части. Когда вы передаете функцию в свойство action формы, React вызывает ее при отправке и передает ей FormData формы. Это означает, что вы можете пропустить создание состояния для каждого поля и просто считать значения в конце.

SignupForm.jsx

Мы познакомились с useActionState в третьей части, вот краткое резюме. Вы передаете ему асинхронное действие и начальное состояние. Он возвращает три вещи:

  • Текущее состояние: я использую его для хранения сообщения об ошибке или null, если все прошло успешно.
  • Обернутое действие: функция, которую вы передаете в свойство action формы.
  • isPending: логическое значение (boolean), которое React устанавливает в true во время выполнения действия, а затем возвращает в false.

Обратите внимание, что здесь нет ref для поля email. У поля есть только имя, и мы считываем его с помощью formData.get('email') при отправке формы. Также нет флага isSubmitting, который нужно переключать вручную, и нет блоков try/finally. React отслеживает состояние ожидания за вас и сбрасывает его, когда действие завершается.

Однако важное предостережение. Неуправляемые поля ввода отлично подходят для простых форм отправки, но как только вам понадобится валидация «на лету» по мере ввода или реакция на значение до отправки, вам снова понадобятся управляемые поля. Оба стиля хороши. Выбирайте тот, который действительно нужен вашему приложению.

useFormStatus: состояние ожидания без «прокидывания» пропсов

Теперь распространенная проблема. У вас есть кнопка отправки, и вы хотите, чтобы она показывала индикатор загрузки во время отправки формы. В примере выше это было легко, потому что isPending находился прямо рядом с кнопкой. Но реальные формы часто разбиваются на части. Ваша кнопка часто находится в своем собственном небольшом компоненте, на несколько уровней ниже формы.

В Vue вы бы передавали проп isSubmitting через каждый уровень или использовали provide и inject, чтобы пропустить промежуточные компоненты. React 19 предоставляет хук, созданный именно для этого. useFormStatus считывает статус ближайшего родительского элемента <form> без использования пропсов.

SubmitButton.jsx

Давайте разберем это. Кнопка запрашивает у useFormStatus текущий статус формы и считывает из него pending. Когда родительская форма отправляется, pending становится true, поэтому кнопка блокирует себя и меняет надпись. Кнопка не получает никаких пропсов. Она просматривает дерево компонентов, находит форму, внутри которой находится, и считывает статус оттуда.

Есть одно правило, на котором многие спотыкаются, так что прочитайте это дважды. useFormStatus сообщает о статусе формы, внутри которой он находится, а не о форме в том же компоненте. Поэтому кнопка должна быть отрендерена внутри <form> и должна быть отдельным компонентом. Если вы вызовете useFormStatus в том же компоненте, который рендерит <form>, он не увидит эту форму. Думайте об этом как об inject в Vue. Он видит только то, что предоставляет предок, поэтому он должен находиться ниже формы в дереве.

Хук импортируется из react-dom, а не из react, потому что он привязан к DOM-элементу <form>. Это легкий импорт, в котором можно ошибиться в первый раз.

useFormStatus дает вам больше, чем просто pending. Он также возвращает:

  • pending: true во время отправки формы.
  • data: отправляемые данные FormData, чтобы дочерний элемент мог прочитать то, что только что отправил пользователь.
  • method и action: HTTP-метод и функция действия, которые использует форма.

В большинстве случаев вам понадобится только pending. Но data полезна, когда вложенному компоненту нужно показать, что отправляется на сервер, например, превью комментария, который кто-то только что опубликовал.

useOptimistic: мгновенный интерфейс «из коробки»

Последний хук — мой любимый, и если вы читали мою предыдущую статью об оптимистичных обновлениях с TanStack Query в Vue, эта идея покажется вам знакомой. Оптимистичное обновление показывает результат сразу, до того, как сервер его подтвердит. Если запрос не удался, вы откатываете изменения. Это делает приложение «отзывчивым».

В статье про TanStack для Vue мы делали это через кэш запросов. Мы записывали новое значение в кэш, сохраняли снимок и откатывали изменения при ошибке. React 19 встраивает ту же идею в хук под названием useOptimistic, и он выполняет откат за вас.

LikeButton.jsx

Давайте разберем это. useOptimistic принимает реальное значение (здесь — количество лайков с сервера) и функцию, которая определяет, как изменить его для оптимистичного отображения. Он возвращает две вещи:

  • optimisticLikes: значение для отображения. Пока выполняется действие, это оптимистичная версия. В остальное время это реальное значение.
  • addOptimisticLike: функция, которую вы вызываете для применения оптимистичного изменения.

Когда пользователь нажимает на кнопку, мы сначала вызываем addOptimisticLike(), поэтому счетчик сразу увеличивается. Затем мы ожидаем выполнения likeAction(), чтобы отправить запрос на сервер. И вот в чем прелесть: если действие завершается ошибкой, React отбрасывает оптимистичное значение и снова показывает реальное. Вам не нужно самостоятельно писать код отката, что всегда было сложным моментом в ручной версии на Vue.

Важный момент, который стоит учитывать при успешном выполнении: счетчик остается увеличенным только в том случае, если обновляется и реальное значение лайков, обычно потому, что действие возвращает актуальный счетчик или данные запрашиваются повторно. Когда действие завершается, useOptimistic синхронизируется с тем реальным значением, которое есть на этот момент. Таким образом, это не место для хранения состояния, а способ показать изменение раньше, пока реальное значение не подтянется.

Функция обновления заслуживает отдельного внимания. Она получает текущее значение, и если вы передаете значение в addOptimisticLike, она получает и его тоже. Так что вы не ограничены только «увеличением на единицу». Очень распространенный сценарий — это список. Вы показываете новый комментарий в списке в тот момент, когда пользователь нажимает «отправить», а затем позволяете серверу догнать состояние. Паттерн такой же, как и со счетчиком. Вы просто описываете, как добавить элемент, вместо того чтобы просто увеличивать число.

Обратите внимание, что это работает только во время выполнения действия. Хук useOptimistic создан для работы в паре с Actions и формами, которые мы использовали на протяжении всей статьи, поэтому он идеально вписывается в тот же поток.

Где должна происходить валидация

Один честный пробел перед завершением. Разработчики на Vue часто полагаются на VeeValidate для валидации, а в React нет ничего подобного «из коробки». React 19 предоставляет вам инфраструктуру для форм, состояние ожидания и оптимистичные обновления, но не дает библиотеку валидации. В React вы либо проверяете значения внутри своего действия и возвращаете ошибки, как мы делали выше, либо подключаете библиотеку. Самая популярная из них — React Hook Form, обычно в сочетании с инструментом для схем, таким как Zod.

Проверка внутри действия звучит лучше, чем кажется. Вы считываете значения из formData, выполняете проверки и возвращаете ошибку, если что-то не так. Затем вы отображаете её точно так же, как состояние ошибки, которое мы использовали в форме регистрации. Для нескольких полей верните объект, где ключами являются имена полей:

Теперь переменная error содержит этот объект, и каждый ввод может отобразить свое собственное сообщение, считывая что-то вроде error?.email рядом с ним. Это ручная работа, но ей легко следовать, и для формы входа или регистрации этого вполне достаточно.

Для больших форм с множеством правил библиотека избавляет вас от написания каждой проверки вручную. Именно здесь на помощь приходят React Hook Form и Zod. Разработчик на Vue, который использовал VeeValidate со схемой, найдет эти идеи знакомыми, даже если API выглядит иначе. Мы подробно разберем их в следующей части, поэтому здесь я буду краток. Пока просто знайте, что вопрос «какую библиотеку валидации выбрать» — это реальный вопрос, который вы решаете в команде React.

Подводя итоги

Формы — это то, где React 19 действительно окупается. Вы отказываетесь от v-model и настраиваете поля ввода самостоятельно с помощью value и onChange — это паттерн контролируемых компонентов. Для простых отправок вы можете пропустить это и позволить форме передать вам FormData через Action.

useFormStatus позволяет вложенной кнопке считывать состояние ожидания формы без использования пропсов, если она находится внутри формы. А useOptimistic дает вам мгновенный отклик интерфейса с автоматическим откатом — та же идея, что и оптимистичные обновления TanStack, которые вы, возможно, уже знаете по Vue, теперь встроена в React.

Подробности можно найти в документации React по useFormStatus и useOptimistic, где показано больше вариантов того, что возвращает каждый хук.

В следующий раз мы завершим серию полноценной шпаргалкой, в которой каждый инструмент Vue сопоставлен с его аналогом в React. Она охватывает маршрутизацию, управление состоянием, получение данных и валидацию, так что у вас будет одна страница, которую удобно держать открытой во время работы.

Удачных отправок!

Хотите легко создавать красиво оформленные и функциональные формы? Ознакомьтесь с компонентом KendoReact Form.

Хотите легко создавать красиво оформленные и функциональные формы? Ознакомьтесь с компонентом KendoReact Form.

О чём эта статья