FormData - My 2nd Favourite Web API
Of the hundreds of Web APIs available in browsers and some server-side runtimes, the FormData class is one of my favourites.
tl;dr
- Use a
<form>
. - Add
names
s to all your inputs - most inputs should have unique names. All inputs withtype="checkbox"
ortype="radio"
should have the same name for logical groups, and should havevalue
s. - Construct a
FormData
object with the form as an argument:new FormData(form)
. - Use the
get
andgetAll
methods to access any values you need!
Using it
At first blush, the
methods available
see kinda useless - why use FormData.get
and FormData.set
when Map
s do the
exact same thing, and plain old objects can do basically the same thing with an
arguable simpler interface? The real power of FormData
comes when used with
the <form>
element. Take the simple register form below:
<form id="register-form"> <!-- Email Input --> <div> <label for="register-email">Email</label> <input type="email" id="register-email" required /> </div> <!-- Password Input --> <div> <label for="register-password">Password</label> <input type="password" id="register-password" required /> </div> <!-- Mailing Lists --> <fieldset> <legend> <p>Select the email lists you want to subscribe to</p> </legend> <label> <input type="checkbox" id="register-mailing-marketing" /> Marketing Emails </label> <label> <input type="checkbox" id="register-mailing-blog" /> Blog Notifications </label> <label> <input type="checkbox" id="register-mailing-spam" /> Constant Spam </label> </fieldset> <button type="submit">Register</button></form>
When the form is submitted you'll need to get the values with JavaScript, which you might do like this:
const registerForm: HTMLFormElement = document.getElementById('register-form')const emailInput: HTMLInputElement = document.getElementById('register-email')const passwordInput: HTMLInputElement = document.getElementById('register-password')const marketingMailingInput: HTMLInputElement = document.getElementById( 'register-mailing-marketing')const blogMailingInput: HTMLInputElement = document.getElementById( 'register-mailing-blog')const spamMailingInput: HTMLInputElement = document.getElementById( 'register-mailing-spam')registerForm.addEventListener('submit', event => { // Remember to prevent the default <form> submission behaviour // if you're handling submissions with JavaScript! event.preventDefault() const email = emailInput.value const password = passwordInput.value const acceptedMailingLists = [] if (marketingMailingInput.checked) { acceptedMailingLists.push('marketing') } if (blogMailingInput.checked) { acceptedMailingLists.push('blog') } if (spamMailingInput.checked) { acceptedMailingLists.push('spam') } doRegister(email, password, acceptedMailingLists)})
This doesn't look that bad, but it's a lot of work that we could avoid.
Consider (almost) the same script but now using FormData
:
const registerForm: HTMLFormElement = document.getElementById('register-form')registerForm.addEventListener('submit', event => { // Remember to prevent the default <form> submission behaviour // if you're handling submissions with JavaScript! event.preventDefault() const formData = new FormData(registerForm) const email = formData.get('email') const password = formData.get('password') const acceptedMailingLists = formData.getAll('mailing-lists') doRegister(email, password, acceptedMailingLists)})
This is good, but there's one pretty big issue with this that I haven't
mentioned yet, which leads into the a (kind of) downside with using FormData
.
The code above doesn't work - not because I suck at JavaScript or because
FormData is too bleeding edge, but because we need to update the origina HTML to
work with FormData
, and be more correct.
In the code snippet above, we used a few strings as arguments to formData.get
and formData.getAll
, but where do these come from? When you use create a
FormData
object with a form
, the keys we use in get
and getAll
map to
the name
attribute for any <input>
s, <select>
s, and <textarea>
s inside
the <form>
. We can fix this issue by adding name
s to the inputs and adding
value
s to the <input>
s with type="checkbox"
:
<form id="register-form"> <!-- Email Input --> <div> <label for="register-email">Email</label> <input name="email" type="email" id="register-email" required /> </div> <!-- Password Input --> <div> <label for="register-password">Password</label> <input name="password" type="password" id="register-password" required /> </div> <!-- Mailing Lists --> <fieldset> <legend> <p>Select the email lists you want to subscribe to</p> </legend> <label> <input name="mailing-lists" value="marketing" type="checkbox" id="register-mailing-marketing" /> Marketing Emails </label> <label> <input name="mailing-lists" value="blog" type="checkbox" id="register-mailing-blog" /> Blog Notifications </label> <label> <input name="mailing-lists" value="spam" type="checkbox" id="register-mailing-spam" /> Constant Spam </label> </fieldset> <button type="submit">Register</button></form>
If you scroll all the way to the bottom, you'll notice that all the checkbox
<input>
s all have the same name
, and they each have a value
. As mentioned
before, the name
is how FormData
knows what to associate the input's value
with, but when there are multiple inputs with the same name
, we can get the
values for all of them with getAll
. Now, to know which checkbox have been
selected, we give them unique value
s.
After making the changes above, everything works! Here's a demo of the complete code in a CodeSandbox:
React and Other Frameworks
If you've used React, you might notice a little issue with the using the code
above - React doesn't (directly) give you access to the DOM elements created.
You could use this weird hacky hook called useRef
, but there's simpler
solution. In fact, you can use this approach in any framework where you have
access to the <form>
s submission event. We can use the currentTarget
property on methods to get access to the element that we added an event listener
to, so currentTarget
for a submit event will be the form! Here's what a
simplified React component using FormData
would look like:
import type React from 'react'export const ExampleForm: React.FC = () => { const handleSubmit: FormEventHandler<HTMLFormElement> = event => { // Remember to prevent the default <form> submission behaviour // if you're handling submissions with JavaScript! event.preventDefault() const formData = new FormData(event.currentTarget) const email = formData.get('email') const password = formData.get('password') const acceptedMailingLists = formData.getAll('mailing-lists') doRegister(email, password, acceptedMailingLists) } return ( <form onSubmit={handleSubmit}> {/* Same form contents as the HTML form above */} </form> )}
Notice how there aren't any useState
s and you don't need to manually wire up
value
and onChange
to control and get values for all of the inputs!
fetch
ing with FormData
When you need to make a fetch
request the values from form, you'll often get
the specific values you need (as above), and make a request using them. However,
if your API accepts application/x-www-form-urlencoded
or multipart/form-data
mime types for the request body (i.e. accepts those values for Content-Type
),
you can set the body
of a fetch
request to the FormData directly! Doing this
also automatically adds the correct Content-Type
header to your request.
const formData = new FormData(document.getElementById('register-form'))fetch('/your-endpoint', { method: 'POST', body: formData,})
This is also the only way I know of right now that allows you to send or upload multiple files in a network request!
On the Server
If you use a server-side framework / environment that uses Web API Request
s
(such as
Next.js' middleware,
Remix and Astro), you can use
FormData
on there too! Request
s have a formData
method that parses the
request body as a FormData
object, similar to how Response.json()
parses a
response's body as JSON.
For example, in Remix your action
s have access to the request and you can
access its body as a FormData
object:
import type { ActionArgs } from 'remix'// This runs on the server!export const action = async ({ request }: ActionArgs) => { const formData = await request.formData() const email = formData.get('email') const password = formData.get('password') const acceptedMailingLists = formData.getAll('mailing-lists') db.createAccount(email, password, acceptedMailingLists)}
A (Somewhat) Nice Hack
FormData
's interface is great, but sometimes you just need object to do the
job. The following snippet converts a FormData
to a plain JavaScript object
where the keys and values correspond to those in your FormData
.
const formData = new FormData(document.getElementById('register-form'))const dataAsObject = Object.fromEntries(formData as any)
Conclusion
FormData
is great. Use it if you can.