VOOZH about

URL: https://blog.logrocket.com/forms-in-react-in-2020/

⇱ Creating forms in React in 2020 - LogRocket Blog


2020-05-18
1497
#react
Kristofer Selbekk
18674
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 Image

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.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Please keep accessibility in mind

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.

Handling forms with the useState Hook

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.

Creating a custom Hook

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.

Handling lots of fields

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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

An alternative approach

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.

When to use what

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.


More great articles from LogRocket:


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.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. 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>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

How to build a virtual engineering team with Gemini CLI subagents

Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.

👁 Image
Emmanuel John
Jun 18, 2026 ⋅ 10 min read

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

👁 Image
Emmanuel John
Jun 17, 2026 ⋅ 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

👁 Image
Chizaram Ken
Jun 16, 2026 ⋅ 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read
View all posts

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 now