Input fields. Text areas. Radio buttons and checkboxes. These are some of the main interaction points we, as developers, have with our users. We put them front and center, users fill them out as best as they can, and with any luck, they’ll send it back to you without any validation errors.
Form handling is an integral part of a large number of web apps, and it’s one of the things React does best. You have a lot of freedom to implement and control those input controls how you want, and there are plenty of ways to achieve the same goal. But is there a best practice? Is there a best way to do things?
This article will show you a few different ways to handle form values in React. We’ll look at useState, custom Hooks, and, finally, no state at all!
Note that we will create a login form with an email and a password field in all of these examples, but these techniques can be used with most types of forms.
Although it doesn’t directly relate to the topic at hand, I want to make sure you remember to make your forms accessible to all. Add labels to your input, set the correct aria-tags for when the input is invalid, and structure your content semantically correct. It makes your form easier to use for everyone, and it makes it possible to use for those that require assistive technologies.
To get us started, let’s have a look at how I typically handle form state. I keep all fields as separate pieces of state, and update them all individually, which looks something like this:
function LoginForm() { const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); api.login(email, password); } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> <div> <label htmlFor="password">Password</label> <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> </form> ); }
First, we create two distinct pieces of state — email and password. These two variables are then passed to their respective input field, dictating the value of that field. Whenever something in a field changes, we make sure to update the state value, triggering a re-render of our app.
This works fine for most use cases and is simple, easy to follow, and not very magical. However, it’s pretty tedious to write out every single time.
Let’s make a small refactor, and create a custom Hook that improves our workflow slightly:
const useFormField = (initialValue: string = "") => { const [value, setValue] = React.useState(initialValue); const onChange = React.useCallback( (e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value), [] ); return { value, onChange }; }; export function LoginForm() { const emailField = useFormField(); const passwordField = useFormField(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); api.login(emailField.value, passwordField.value); }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" {...emailField} /> </div> <div> <label htmlFor="password">Password</label> <input type="password" id="password" {...passwordField} /> </div> </form> ); }
We create a custom Hook useFormField
that creates the change event handler for us, as well as keeps the value in state. When we use this, we can spread the result of the Hook onto any field, and things will work just as it did.
One downside with this approach is that doesn’t scale as your form grows. For login fields, that’s probably fine, but when you’re creating user profile forms, you might want to ask for lots of information! Should we call our custom Hook over and over again?
Whenever I stumble across this kind of challenge, I tend to write a custom Hook that holds all my form state in one big chunk. It can look like this:
function useFormFields<T>(initialValues: T) { const [formFields, setFormFields] = React.useState<T>(initialValues); const createChangeHandler = (key: keyof T) => ( e: React.ChangeEvent<HTMLInputElement>, ) => { const value = e.target.value; setFormFields((prev: T) => ({ ...prev, [key]: value })); }; return { formFields, createChangeHandler }; } export function LoginForm() { const { formFields, createChangeHandler } = useFormFields({ email: "", password: "", }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); api.login(formFields.email, formFields.password); }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" value={formFields.email} onChange={createChangeHandler("email")} /> </div> <div> <label htmlFor="password">Password</label> <input type="password" id="password" value={formFields.password} onChange={createChangeHandler("password")} /> </div> </form> ); }
With this useFormFields
Hook, we can keep on adding fields without adding complexity to our component. We can access all form state in a single place, and it looks neat and tidy. Sure, you might have to add an “escape hatch” and expose the underlying setState
directly for some situations, but for most forms, this’ll do just fine.
So handling the state explicitly works well, and is React’s recommended approach in most cases. But did you know there’s another way? As it turns out, the browser handles form state internally by default, and we can leverage that to simplify our code!
Here’s the same form, but letting the browser handle the state:
export function LoginForm() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); api.login(formData.get('email'), formData.get('password')); }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" /> </div> <div> <label htmlFor="password">Password</label> <input type="password" id="password" name="password" /> </div> <button>Log in</button> </form> ); }
Now, that looks simple! Not a single Hook in sight, no setting the value, and no change listeners either. The best part is that it still works as before – but how?
You might have noticed we’re doing something a bit different in the handleSubmit
function. We are using a built-in browser API called FormData. FormData is a handy (and well supported) way to get the field values from our input fields!
We get a reference to the form DOM element via the submit event’s target attribute and create a new instance of the FormData class. Now, we can get all fields by their name attribute by calling formData.get(‘name-of-input-field’).
This way, you never really need to handle the state explicitly. If you want default values (like if you’re populating initial field values from a database or local storage), React even provides you with a handy defaultValue
prop to get that done as well!
We often hear “use the platform” used as a slight, but sometimes the platform just comes packing a punch.
Since forms are such an integral part of most web applications, it’s important to know how to handle them. And React provides you with a lot of ways to do just that.
For simple forms that don’t require heavy validations (or that can rely on HTML5 form validation controls), I suggest that you just use the built-in state handling the DOM gives us by default. There are quite a few things you can’t do (like programmatically changing the input values or live validation), but for the most straightforward cases (like a search field or a login field like above), you’ll probably get away with our alternative approach.
When you’re doing custom validation or need to access some form data before you submit the form, handling the state explicitly with controlled components is what you want. You can use regular useStateHooks, or build a custom Hook solution to simplify your code a bit.
It’s worth noting that React itself recommends that you use controlled components (handling the state explicitly) for most cases – as it’s more powerful and gives you more flexibility down the line. I’d argue that you’re often trading simplicity for flexibility you don’t need.
Whatever you decide to use, handling forms in React has never been more straightforward than it is today. You can let the browser handle the simple forms while handling the state explicitly when the situation requires it. Either way – you’ll get the job done in less lines of code than ever before.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
13 Replies to "Creating forms in React in 2020"
Nice post, wish when I read all these forms in react blogs though that they’d include nice ways the include validation
Great article 👍
There is a small typo in the first example. You entered username.. probably ment email
Coulda mentioned somewhere that you’re using typescript. Cheers.
I think, the approach you used for get data using FormData, it’s antipattern in react. Please see controlled components vs uncontrolled components.
I feel you – but validation in React is a huge topic by itself. I typically start out with using HTML5’s form validation. If you need more customization, I wrote an article series about how you can write your own validation logic: https://www.smashingmagazine.com/2019/05/react-validation-library-basics-part1/
If Formik is a great library, as is react-form-hook. Best of luck!
I believe your useFormFields example isn’t working that way…
You missed submit button in the first example.
Hi Oleg! `type=”submit”` is the default for components, so you don’t have to specify it explicitly
I think what the author is trying to say throughout the article is that in simple cases, the overhead of controlled components doesn’t bring any additional benefits. Just using uncontrolled components alone isn’t an anti pattern.
Awesome! great explanation (L)
Hi ,
I use a custom hook, but the problem is that I also have a large list in the component. When I do the onChange operation, this list becomes re render again and greatly reduces performance.
Your code works fine until a checkbox is added. The formData doesn’t seem to return a true/false value.
const formData = new FormData(e.target as HTMLFormElement);
The formData.get(‘registerMe’) returns ‘on’ instead of true/false. I cannot think of any other way except to access the checkbox value directly:
e.target.elements[‘registerMe’].checked
So wouldn’t it be better to access the elements directly rather than through the FormData? You already have a reference to the form and can access the values.
Awesome write up. Be nice if it was not typescript though.