React и FormData

Источник: «React and FormData»
Узнайте о новейшем и одновременно старейшем стандарте React для доступа к данным формы, а также о том, как использовать его с TypeScript.

Когда изучаете, как получить доступ к данным формы в React, исторически вы должны были узнать об управляемых и неуправляемых полях. Позже можете начать использовать сторонние абстракции, такие как Formik или React Hook Form, использующие управляемые и неуправляемые методы под капотом. В любом случае конечной целью является сбор данных формы. В случае с управляемой формой ваши данные — это ваше состояние. В случае с неуправляемой нужно собирать значения формы самостоятельно, и обычно разработчики выбирают для этого рефы:

function onSubmit(event: React.FormEvent) {
event.preventDefault()

// Соберите неуправляемые поля формы с помощью рефов.
// Рефы дают прямой доступ к полям ввода в DOM
const formValues = {
name: nameRef.current.value
email: emailRef.current.value
}
}

Все поля форм в React должны быть либо управляемыми, либо неуправляемыми, потому что вы либо добавляете value в проп, либо нет. FormData, стандарт JavaScript с 2010 года, — это способ доступа к данным формы, независимо от того, управляемая она или неуправляемая, но большинство предпочитает неуправляемую.

Несмотря на то что FormData можно было использовать в React с самого начала, в последние несколько лет наблюдается всплеск его популярности. Позже расскажем, как его используют и продвигают современные функции React 19.

FormData

С FormData не нужны рефы для получения значений неуправляемых форм. Вместо этого можно просто считывать значения формы непосредственно из event.target:

function onSubmit(event: React.FormEvent) {
event.preventDefault()
const formData = new FormData(event.target)

const formValues = {
name: formData.get('name')
email: formData.get('email)
}
}

По некоторым причинам TypeScript жалуется, если вы используете event.target, и хочет, чтобы вы использовали event.currentTarget. Однако чтобы вы знали, эти два параметра часто обозначают одно и то же, и чаще всего не имеет значения, какой из них используется. Но сейчас будем использовать event.currentTarget, поскольку многие разработчики React используют TS.

Добавление имён

Не забудьте добавить имена к полям input, чтобы FormData работал:

// ✅ Работает, потому что input имеет совпадающее имя
const email = formData.get('email')
<input type="text" name="email" />

Доступ к данным без геттеров

Нельзя ли сделать что-то вроде этого, чтобы извлечь все данные из формы без геттеров?

// ❌ Не работает
const formValues = { ...formData }

Экземпляр объекта formData более непрозрачен и не является тем объектом, который можно смешивать с объектными литералами. Если вывести его через console.log, то мы не увидим значений:

console.log(formData) // output: `FormData {} [[Prototype]]: FormData`

Object.fromEntries()

Можно обойтись без геттеров и распаковать значения в более простой объект, например, так:

const formValues = Object.fromEntries(formData)
console.log(formValues) // output: { name: 'my name', email: 'name@someemail.com' }

Однако при использовании TypeScript вы не получите желаемых типов.

Проблемы с FormData и TypeScript

Несмотря на то что значение поля input будет строкой, а если пользователь не введёт никакого значения, то это будет пустая строка, TypeScript говорит, что тип, возвращаемый геттером, будет FormDataEntryValue | null.

const quantity = formData.get('quantity')
typeof quantity // FormDataEntryValue | null

Это может привести к тому, что для выполнения такого простого действия, как преобразование введённого пользователем значения в число, придётся приложить немало усилий:

// Утверждение string или null
const quantity = formData.get('quantity') as string | null

// Затем предоставьте значение по умолчанию на случай, если будет возвращено ложное null значение
const quantity = (formData.get('quantity') as string | null) || 0

// Теперь можно передать в parseInt, чтобы получить целочисленную версию пользовательского ввода
const quantity = parseInt((formData.get('quantity') as string | null) || 0)

С Object.fromEntries дело обстоит не лучше. Он только знает, что это объект с неизвестным количеством строковых ключей со значениями FormDataEntryValue:

const formValues = Object.fromEntries(formData)
typeof formValues // { [k: string]: FormDataEntryValue }

Устранение проблем с Zod

Zod — это основанный на схемах валидатор JavaScript, похожий на Yup и Joi. Но в отличие от своих предшественников, Zod был специально написан для работы с TypeScript. Поскольку это не учебник по Zod, постараемся как можно короче рассказать о его возможностях.

Идея использования Zod заключается в том, что вам, вероятно, всё равно понадобится валидация, так почему бы не получить лучшие типы без утверждений?

Когда вы передаёте этот непрозрачный объект formValues в валидатор схемы, Zod проверяет его на основе схемы, которую вы написали (здесь не показано), а затем возвращает ваши данные, но в форме, совместимой с правилами вашей схемы, которую он только что передал:

const formValues = Object.fromEntries(formData) // ❌ TYPE: { [k: string]: FormDataEntryValue }

const results = myFormSchema.safeParse(formValues)
if (results.success) {
results.data // ✅ TYPE: { email: string, quantity: number }
console.log(results.data.email) // name@someemail.com
console.log(results.data.quantity) // 5
} else {
// Делаете с results.errors всё, что хотите
}

FormData и React 19

Современное API React побуждает к использованию и изучению FormData. В React 19 вы можете отказаться от onSubmit в пользу action:

function MyForm() {
function formAction(formData: FormData) {
// Вместо события мы получаем экземпляр formData
}

return <form action={formAction}>...</form>
}

Когда React вызывает вашу функцию formAction, она передаёт экземпляр FormData. Аналогичное использование FormData наблюдается в хуках React 19, таких, как useActionState.

В фреймворках

Remix, который вскоре будет преобразован в React Router 7, известен тем, что поддерживает такие веб-стандарты, как FormData, Request и Response. Когда форма отправлена, её данные доступны на сервере через стандартный экземпляр Request. Согласно MDN, вы можете получить FormData из запроса.

Такой способ работы с FormData в Remix соответствует стандартам JavaScript:

// В Remix экшен "ловит" запросы POST/PUT/DELETE.
export async function action(request: Request) {
const formData = await request.formData()
// ...
}

Спасибо за прочтение!

Комментарии


Дополнительные материалы

Предыдущая Статья

12 вопросов для собеседования по Laravel: Работодателю и кандидату

Следующая Статья

Синтаксис Pest vs PHPUnit: Примеры expect()