Form Actions
By Flavio Copes
Learn how React form actions simplify handling form submissions by passing the FormData object straight to your action, with no onSubmit handler or refs.
Form actions let you pass a function directly to a form’s action prop in React. When the form is submitted, React calls that function and hands it the form’s data as a FormData object. No onSubmit handler, no preventDefault(), no state for each input field.
To appreciate how much this cleans things up, let’s look at what we did before.
The classic pattern
Within a component you might need to respond to a form submission by sending data to a server. The common pattern: track each field with useState, prevent the default browser submission, then send the data with fetch():
import { useState } from 'react'
export const Demo = () => {
const [fullName, setFullName] = useState('')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ fullName }),
})
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
type='text'
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
Every input needs its own state and its own onChange. And if you forget e.preventDefault(), the browser reloads the page on submit.
Using FormData
Same example, using FormData, which removes the need to track each individual input field value with useState:
import { useRef } from 'react'
export const Demo = () => {
const formRef = useRef<HTMLFormElement>(null)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (formRef.current) {
const formData = new FormData(formRef.current)
await fetch('/api/register', {
method: 'POST',
body: formData,
})
}
}
return (
<div>
<form ref={formRef} onSubmit={handleSubmit}>
<input
type='text'
name='fullName'
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
Better, but we still handle the browser event manually, and we keep a ref around just to read the form.
Using an action
The same thing can now be performed using an action, available since React 19:
export const Demo = () => {
async function submitAction(formData) {
await fetch('/api/register', {
method: 'POST',
body: formData,
})
}
return (
<div>
<form action={submitAction}>
<input
type='text'
name='fullName'
/>
<button type='submit'>Submit</button>
</form>
</div>
)
}
Look how much simpler the code is. We don’t track individual input field state, we don’t respond to a browser event directly, and we don’t pass a form ref around, because the action is directly passed the FormData object.
Inside the action, you read single values by name:
const fullName = formData.get('fullName')
One pitfall: inputs need a name
FormData is built from the name attributes of the form fields. An input without a name doesn’t show up at all, and formData.get('fullName') returns null.
So if your action receives empty data, check the name attributes first. It’s the most common mistake with this pattern, because with the useState version the attribute wasn’t needed.
One more behavior to know: after the action completes, React resets the form’s uncontrolled fields, like a native form submission would. The text input above clears itself once the data is sent.