Creating React Forms in 2020

Hi, Habr. I present to you the translation of the article "Creating forms in React in 2020" by Kristofer Selbekk

image


Input fields Text areas. Radio buttons and checkboxes. Here are some of the main points of interaction that we, as developers, have with our users. We place them on the site, users fill them out and if we are lucky, then the completed forms come to us without validation errors.

Form processing is an integral part of a large number of web applications, and this is one of the things that React does best. You have great freedom to create and process forms. But is there a better way to do this?

Please note that in all these examples we will create a login form with an email and password field, but these methods can be used with most types of forms.

Remember the rules of good form


Although this is not directly related to the topic under discussion, I want to make sure that you have not forgotten to make your forms understandable to everyone. Add tags to your input fields, set the correct aria attributes for cases where input is invalid, and structure your content semantically correctly. This makes it easy to use your form for both users and developers.

Processing Forms Using State Hook


To get started, let's see how I usually work with form state. I save all the fields as separate state elements and update them all separately, 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, I create two separate parts of the state - username and password. These two variables are then transferred to the corresponding input field, determining the value of this field. Whenever something in a field changes, we are sure to update the state value, causing a redraw of our application.

This form processing works fine in most cases and is simple and easy to use and understand. However, it is rather tedious to write such a construct every time.

Creating a custom hook


Let's do a little refactoring and create our own hook, which will slightly improve our code:

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 useFormField hook that creates an onChange event handler for us, and also stores the value in the form state. In this case, we can use our hook with all form fields.

Processing a large number of fields


The disadvantage of this approach is that it does not scale well as your form grows. For small forms like login, this is probably good, but when you create user profile forms you may need to request a lot of information! Should we call our hook again and again?

Whenever I encounter a similar situation, I tend to write a custom hook that stores all my forms in one large fragment. It might look like this:

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 the useFormFields hook, we can continue to add fields without adding complexity to our component. We can access all form states in one place, and our code looks neat and concise. Of course, you probably need to use setState for some situations, but for most forms the above technique is just fine.

Alternative approach


Thus, state handling works fine, and in most cases this is the preferred approach in React. But did you know that there is another way? It turns out that the browser by default processes the internal state of the form, and we can use this to simplify our code!

Here is the same form, but allowing the browser to handle the state of the form:

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>
  );
}

Look how easy it is! Not a single hook was used, there is no initial value setting, there is no onChange handler. The best part is that the code still works as before - but how?

You may have noticed that we are doing something a little different in the handleSubmit function. We use the built-in browser API called FormData. FormData is a convenient (and well-supported) way to get values ​​from our input fields!

We get a link to the form in the DOM through the target attribute of the submit event and create a new instance of the FormData class. Now we can get all the fields by their name attribute by calling formData.get ("input field name").

That way, you never need to explicitly handle the state of the form. If you need default values ​​(for example, if you change the initial field values ​​from a database or local storage), React provides you with a convenient defaultValue option for this.

When to use each approach


Since forms are an integral part of most web applications, it is important to know how to handle them. And React provides you with many ways to do this.
For simple forms that do not require rigorous validation (or that can rely on HTML5 form validation controls ), I suggest you simply use the built-in state handling that the DOM provides to us by default. There are quite a few things that you cannot do (for example, programmatically change input values ​​or check in real time), but in the simplest cases (for example, a search field or a login field, as indicated above), you will probably find it suitable our alternative approach using the browser API.

When you perform a custom check or you need access to some form data before submitting it, you need to explicitly process the state using controlled components. You can use regular useStateHooks or create a custom hook to simplify your code a bit.

It is worth noting that React developers recommend the use of controlled components (explicitly handle the state of the component) in most cases - since this approach gives you more flexibility in the future. In my opinion, developers often sacrifice simplicity for the sake of flexibility, which they often do not need.

Whatever approach you decide to use, React form processing has never been as easy as it is today. You can let the browser handle simple forms, while at the same time explicitly processing the state of the forms when the situation requires it. I hope the above methods will be useful and help you solve the problem by reducing the number of lines in your code.

All Articles