Handling Forms in React
By Flavio Copes
Learn how to handle forms in React with controlled components, useState, uncontrolled file inputs, and React Hook Form for bigger forms.
Forms are one of the few HTML elements that are interactive by default.
They were designed to allow the user to interact with a page.
Common uses of forms?
- Search
- Contact forms
- Shopping carts checkout
- Login and registration
- and more!
Using React we can make our forms much more interactive and less static.
There are two main ways of handling forms in React, which differ on a fundamental level: how data is managed.
- if the data is handled by the DOM, we call them uncontrolled components
- if the data is handled by the components we call them controlled components
As you can imagine, controlled components is what you will use most of the time. The component state is the single source of truth, rather than the DOM. But sometimes you are forced to use uncontrolled components, for example when using some form fields that are inherently uncontrolled because of their behavior, like the <input type="file"> field.
When an element state changes in a form field managed by a component, we track it using the onChange attribute.
Today you write forms as function components and keep the field value in state with useState. This example runs on React 19:
import { useState } from 'react'
const Form = () => {
const [username, setUsername] = useState('')
const handleChangeUsername = event => {
setUsername(event.target.value)
}
const handleSubmit = event => {
event.preventDefault()
alert(username)
}
return (
<form onSubmit={handleSubmit}>
Username:
<input
type="text"
value={username}
onChange={handleChangeUsername}
/>
<input type="submit" value="Submit" />
</form>
)
}
value={username} makes the input controlled. Every keystroke calls setUsername, React re-renders, and the input always shows what is in state.
Validation can live in the change handler: you have the old value and the new one. You can reject a value that is not valid, and tell the user why.
HTML Forms are inconsistent. They have a long history, and it shows. React however makes things more consistent for us, and you can get (and update) fields using its value attribute.
Here’s a textarea, for example:
<textarea value={address} onChange={event => setAddress(event.target.value)} />
The same goes for the select tag:
<select value={age} onChange={event => setAge(event.target.value)}>
<option value="teen">Less than 18</option>
<option value="adult">18+</option>
</select>
Uncontrolled fields and file inputs
<input type="file"> works differently. The browser owns that value, so you read it through a ref instead of putting it in state.
With a function component you can use useRef for that:
import { useRef } from 'react'
const FileInput = () => {
const curriculum = useRef(null)
const handleSubmit = event => {
event.preventDefault()
alert(curriculum.current.files[0].name)
}
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={curriculum} />
<input type="submit" value="Submit" />
</form>
)
}
This is the uncontrolled components way. The file lives in the DOM, not in React state.
Class components (legacy)
Older tutorials show the same controlled form with a class and this.state. It still works, but you have to bind handlers (or use arrow methods) so this is available:
class Form extends React.Component {
constructor(props) {
super(props)
this.state = { username: '' }
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(event) {
this.setState({ username: event.target.value })
}
handleSubmit(event) {
event.preventDefault()
alert(this.state.username)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.username}
onChange={this.handleChange}
/>
<input type="submit" value="Submit" />
</form>
)
}
}
You only need this to read old code. Write new forms with function components and hooks.
Form Actions in React 19
React 19 added one more way to handle a submit. Instead of onSubmit, you pass a function to the form’s action prop. React calls it with a FormData object, so you don’t need event.preventDefault() and you don’t need state for every field:
const Search = () => {
const search = formData => {
alert(formData.get('query'))
}
return (
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
)
}
The fields here are uncontrolled. React reads them by name when the form is submitted, and resets them after the action succeeds. Two hooks go with this: useActionState keeps the result of the last submit (an error message, for example) and useFormStatus tells a button inside the form whether a submit is pending. The form reference covers both.
Form libraries
Beyond those basics, bigger forms need validation, error messages, and less boilerplate.
The most used library for this is React Hook Form. Version 7 is the current stable release as of September 2026, and it gets about 40 million npm downloads a week. It keeps the fields uncontrolled, so typing does not re-render the whole form, and the API is small:
npm install react-hook-form@7
You register each field, and handleSubmit runs the validation before calling your function:
import { useForm } from 'react-hook-form'
const SignupForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm()
const onSubmit = data => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username', { required: true })} />
{errors.username && <p>Username is required</p>}
<input type="submit" value="Submit" />
</form>
)
}
data is an object with one key per registered field, { username: 'flavio' } here.
Formik was the popular choice when I first wrote this post. It still gets releases (2.4.9 in November 2025 added a React 19 fix), but it moves slowly and has about a tenth of the downloads. If a codebase already uses it, keep it. For a new form, start with React Hook Form.
Want me to talk about your product? You can sponsor this site.