Blog Hero Background

React and FormData

Learn React's newest and yet oldest standard for accessing form data, and the tricks to use it with TypeScript.


User Avatar
Brad WestfallSay hi on Twitter
September 08, 2024
ReactJavaScriptForms

See Our Public Workshops:

When you learn how to access form data in React, historically you would have learned about controlled and uncontrolled fields. Later you might start to use third party abstractions like Formik or React Hook Form which employ controlled or uncontrolled techniques under the hood. Either way, the end goal is to collect your form's data. With controlled, your data is your state. With uncontrolled you need to collect the form values yourself, and usually devs choose refs for that:

function onSubmit(event: React.FormEvent) {
event.preventDefault()
// Collect uncontrolled form fields with refs. The refs are
// giving us direct access to the input fields in the DOM
const formValues = {
name: nameRef.current.value
email: emailRef.current.value
}
}

All form fields in React must either be controlled or uncontrolled because you're either adding a value prop or you're not. FormData, a JavaScript standard since 2010 is a way to access your form's data whether it's controlled or uncontrolled, but most are preferring uncontrolled.

Even though FormData has been possible to use with React since the beginning, we've seen it's popularity spike in the last few years. Later, we'll show you how it's being adopted and pushed by modern React 19 features.

FormData

With FormData, you don't need refs to get the values of uncontrolled forms. Instead, you can just read the form values directly off 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)
}
}

For certain reasons, TypeScript complains if you use event.target and wants you to use event.currentTarget. Just so you know though, these two can often refer to the same thing and most often it doesn't matter which you use. but we'll use event.currentTarget now since many React developers are doing TS.

Adding Names

Be sure to add names to the input fields in order for FormData to work:

// ✅ Works because input has matching name
const email = formData.get('email')
<input type="text" name="email" />

Accessing data without getters

Couldn't we just do something like this to extract all the form data without getters?

// ❌ Wont work
const formValues = { ...formData }

The object instance formData is more opaque and is not the same kind of object we can mix with object literals. If we console.log it, we won't see values either:

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

Object.fromEntries()

You can avoid the getters and unpack the values into a more plain object like this:

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

However, doing so with TypeScript does not give you the types you would have wanted.

Problems with FormData and TypeScript

Even though the value of an input field will be a string, and if the user types no value it will be an empty string, TypeScript says the type returned from the getter is FormDataEntryValue | null.

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

This can lead to a lot of messy work to do something simple like convert the user's input to a number:

// Assert string or null
const quantity = formData.get('quantity') as string | null
// Then provide default incase falsy null value is returned
const quantity = (formData.get('quantity') as string | null) || 0
// Now we can pass to parseInt to get the integer version of the user's input
const quantity = parseInt((formData.get('quantity') as string | null) || 0)

With Object.fromEntries it's not better. They only know it's an object with an unknown amount of string-keys with FormDataEntryValue values:

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

Problems fixed with Zod

Zod is a schema-based JavaScript validator similar to others like Yup and Joi. But unlike its predecessors, Zod was very specifically written to work well with TypeScript. Since this isn't a Zod tutorial, we'll try to convey Zod's power as briefly as we can.

The idea of using Zod here is that you'll probably need validation anyways, why not get better types without assertions?

When you pass this opaque formValues object into the schema validator, Zod validates it based on the schema you wrote (not shown here) but then also gives you back your data but in a type-safe way according to the rules of your schema that it just passed:

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 {
// Do what you want with results.errors
}

FormData and React 19

Modern React API's are encouraging you to use and learn FormData as well. In React 19, you can omit onSubmit in favor of action:

function MyForm() {
function formAction(formData: FormData) {
// We are given an instance of formData instead of event
}
return <form action={formAction}>...</form>
}

When React calls your formAction function, they'll pass you an instance of FormData. We see similar usage of FormData in React 19 hooks like useActionState

In Frameworks

Remix, which will soon be converged into React Router 7 is known for embracing web standards like FormData, Request, and Response. When a form is submitted, it's data is available on the server via a standard Request instance. According to MDN, you can get FormData off the request.

This "Remix" way of handling form Data is inline with JavaScript standards:

// In Remix, the action "catches" POST/PUT/DELETE requests
export async function action(request: Request) {
const formData = await request.formData()
// ...
}

Thanks for reading!


Instead of having comments here in our blog, we've tweeted about this post so you can comment there if you wish.

View on Twitter

Photo by Devon Janse van Rensburg on Unsplash

Subscribe for updates on public workshops and blog posts

Don't worry, we don't send emails too often.

i love react
© 2024 ReactTraining.com