React and FormData
Learn React's newest and yet oldest standard for accessing form data, and the tricks to use it with TypeScript.
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 DOMconst formValues = {name: nameRef.current.valueemail: 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 nameconst 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 workconst 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 nullconst quantity = formData.get('quantity') as string | null// Then provide default incase falsy null value is returnedconst quantity = (formData.get('quantity') as string | null) || 0// Now we can pass to parseInt to get the integer version of the user's inputconst 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.comconsole.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 requestsexport 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