Forms in React: Under the Hood
A (not so) short discussion of form handling in React.
Published on January 25, 2023
by
Filed under development
Handling forms using React can be quite a task, especially when done manually. But no worries! This blog is here to help.
This is actually the first of two blogs covering form handling. Since there’s lots to say, this one will cover the basics, and the next one will be a bit more advanced. So, let’s make an introduction, show some code samples, and provide a detailed insight into what manual handling of forms actually means.
As many developers know, the best way to save time (and nerves) is to take advantage of already existing libraries or packages, and use electronic forms. To be able to properly use form-handling packages, it’s important to know how they work, what they provide and do initially.
Almost any functionality can be easily implemented in an application using a certain package on the npm platform. But, it’s often forgotten which problem the package initially solves, as well as what it enables and does in the background. The same happens with packages used for handling forms. It’s important to know the principles in order to use the right package in the sea of similar ones.
I’ll assume that some topics, like function definition, JSX, React hooks, prop drilling, and defining state in React, are well known to everyone here. If not, you can always go back to the React manual and learn more.
In order for a website to be successful, it has to do much more than just provide information. Visitors should be able to interact with it, express their wishes, and have an overall positive user experience. Forms display, collect, and send different types of data. They are then used as input parameters to execute specific predefined tasks and functions. They are a crucial part of many apps and websites.
Hint: Forms are just a very clever substitute for the person who used to carry papers in their hands, knew exactly what data has to be written on it, and where all those papers need to be sent.
Further down in the text, I’ll focus on showing form handling on a simple example, as well as what’s done behind the scene. I’ll use a simple login form in which you’ll only need to enter your e-mail address and password. Based on this, you’ll see all the steps every form must have in modern web applications.
As already mentioned, forms are very complex to manipulate manually. They have quite a lot of functionalities, and even experienced developers can find them tricky.
Form handling in any technology should cover:
setting up and updating the state for form values, form errors, and form validity
creating validation functions
handling submissions.
No doubt, working manually would provide us with more bugs, but we’d be familiar with the processes that are behind form handling. Form interaction with users is one of the most challenging problems when developing applications.
We use the React useState hook to set the state for form values, errors, validity, and isSubmiting. Any form needs to know those four in order to properly handle user input:
values to keep the written text, number, or any other enabled type
errors to be able to set custom error messages
validity property to keep track of whether the input is correct or not
isSubmitting property to enable or disable form submission.
All we have to do at the end of the day is to return a JSX element that would represent the view layer, the only thing React really cares about.
const LoginForm = () => {
const [form, setForm] = useState({
values: {
email: "",
password: "",
},
errors: {
email: "",
password: "",
},
validity: {
email: false,
password: false,
},
isSubmitting: false,
});
const {values, errors, isSubmitting} = form;
const handleChange = (event) => {
values[event.target.name] = event.target.value;
setForm({ ...form, values });
};
return (
<form>
<div>
<label>Email address</label>
<input
type="email"
name="email"
placeholder="Enter email"
onChange={handleChange}
value={values.email}
/>
<div>{errors.email}</div>
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
placeholder="Password"
onChange={handleChange}
value={values.password}
/>
<div>{errors.password}</div>
</div>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
};
The key to form handling is the validation process and submission. The validation process is the core, and we should keep a few things in mind simultaneously. There are quite a few rules when it comes to validation:
users have to be able to input only a certain type of content (eg. numbers, date, text etc.) or be able to select components
one input often affects the appearance or values of another one
editing certain inputs should sometimes be disabled
the content in any dropdown menu must be constrained – the testing point has to be developed for every input in order to be validated correctly
if any mandatory field is not filled correctly, the submission action can not be allowed.
For a better UX, we must send a certain response to the user. It would be great if the form would handle error messages, and display them when the error occurs. For those and many more reasons, handlers have to be created. It’s very important to call the form validation method every time the state updates. Normally, we do that by calling the validation method inside the handleChange method.
const handleValidation = (target) => {
const { name, value } = target;
const { errors, validity } = form;
const fieldValidationErrors = errors;
const fieldValidity = validity;
const isEmail = name === "email";
const isPassword = name === "password";
const emailTest = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
fieldValidity[name] = value.length > 0;
fieldValidationErrors[name] = fieldValidity[name]
? ""
: `${name} is required and cannot be empty`;
if (fieldValidity[name]) {
if (isEmail) {
fieldValidity[name] = emailTest.test(value);
fieldValidationErrors[name] = fieldValidity[name]
? ""
: `${name} should be a valid email address`;
}
if (isPassword) {
fieldValidity[name] = value.length >= 8;
fieldValidationErrors[name] = fieldValidity[name]
? ""
: `${name} should be 8 characters minimum`;
}
}
setForm({
...form,
formErrors: fieldValidationErrors,
formValidity: validity,
});
};
The last step in setting basic form functionalities using plain React is to check validity values in the handleSubmit method. We do that during the submission process and pass that method to the onSubmit property. If there are any false values inside the validity object, form submission must be disabled, and the validation method has to be called again.
const handleSubmit = (event) => {
event.preventDefault();
setForm({ ...form, isSubmitting: true });
const { formValues, formValidity } = form;
if (Object.values(formValidity).every(Boolean)) {
alert("Form is validated! Submitting the form...");
setForm({ ...form, isSubmitting: false });
} else {
for (let key in formValues) {
let target = {
name: key,
value: formValues[key]
};
handleValidation(target);
}
setForm({ ...form, isSubmitting: false });
};
You’re probably asking yourself: Does React even handle forms? How can it help me to make this process quicker and easier? The answer is, unfortunately, no it doesn’t, and it can’t. It doesn’t have any module, component, behavior, or pattern for working with forms. That paradigm inside React doesn’t exist.
It doesn’t handle forms by itself, React won’t even help you when handling and validating them, it just renders the view. React only provides the view layer for our application, the basic necessity in making form components. Components, state, and props are like puzzle blocks that have to piece themselves together to build a working form.
Therefore, React is very effective for this task due to its modularity, and can cooperate with more or less any form-handling packages. It only makes sure that the UI changes appropriately as a response to the user’s activity.
In the shown example, only two input parameters were used, email and password. Just imagine if we added more inputs to the form. The code would be significantly more complicated. As a result, manipulating forms using only React feels like a nightmare. Various developers have created libraries that have ready-made data validation methods, simple ways to define schemes and form manipulation. Some of the most used libraries that allow easier manipulation of forms in React are React-Hook-Form, Formik, and React-Final-Form. I’ll be writing more about the mentioned packages in another blog soon.
Fact: Get rid of tedious work and let the React form library manage everything.
Before we take a deeper dive into React-based form libraries, it would be very helpful to define what kinds of inputs exist in the React form. Inputs in React can be one of two types: controlled or uncontrolled. They differ in multiple ways. When using the controlled component, the value is located in the event object, while, when using the uncontrolled component, the value can be retrieved using useRef hook.
With a controlled input, the code has to be written to respond to keypresses, store the current value somewhere, and pass the value back to the input. It takes more manual work to wire these up, but they offer the most control.
const Form = () => {
const [email, setEmail] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
console.log('email:', email);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
Each input tag has a couple of props that make the magic happen:
the value prop that decides what content to display, usually the value prop value is just the state value, like in the example
the OnChange prop function that gets called every time the user changes the input value. It receives an event, and the current input value stored within is saved inside the component state.
This means that with every keypress, the component will re-render the whole form. If an application screen has a form with tons of inputs, this re-rendering might affect form functionality, especially on slower devices. One can ask if the form can be optimized in order to limit re-renders. Maybe the best practice would be to divide that number of inputs on multiple screens and forms.
Hint: If the form can’t work properly, as it has to work with tons of input, imagine what users might think seeing that form on their screens.
When using uncontrolled inputs, React only renders the input and the browser does the rest. Uncontrolled inputs manage their value by keeping it in the input’s DOM node. That value is fetched using the useRef hook. References tie JSX and actual DOM, which the browser presents, letting React components get access to the DOM. The ref object just holds a reference to the DOM node of the certain tag.
import React, {useRef} from 'react';
const Form = () => {
const emailRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
//console log all values you want
console.log('email:', emailRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
ref={emailRef}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
What components should we use? Unfortunately, the answer is not simple. Both components can be used. Actually, in some situations, both are used! Controlled inputs use a more React-ish way of changing state, and the view is changed as well. The controlled components seem perfect when validation is fired on every keypress or when you want to prevent the user from typing a certain character or sign. Additionally, you can use them to disable the form submit button. As long as the form is not valid or when there is a functionality, that one input is depending on another.
There are two disadvantages of using controlled components:
form manipulating seems a bit harder and more complex, which means the code needs to hold the state, and the change handler function needs to exist at every input
they require more boilerplate by writing the props inside them. Also, every time a key is pressed, React calls the function in the onChange prop, which sets the state. Setting the state causes the component and its children to re-render.
A big point in favor of using uncontrolled inputs is that the browser assists them a lot. It takes care of updating values – every keypress bypasses React and goes directly to the browser. Also, there is less boilerplate than in the controlled components.
The decision of which component type to use is, of course, completely yours, sometimes people switch it up. The key thing is to know when it’s better to use one type or the other.
Everything mentioned so far (bearing in mind that we actually took a very simple case as an example) clearly shows how extensive and difficult it is to handle a form manually. There are many steps and tasks included in such a complete handling, and it’s quite easy to forget something or to do it incorrectly.
So, it’s much easier to use a certain form package, simplify the code, and pass it on to other developers, than to use plain React. Working without a package seems like a waste of time, especially when those packages (more or less) provide everything you’d otherwise type manually.
The advantage of using packages is that they’re tested, widespread, they perform their function, and provide various methods with which you can write code more easily. Also, they reduce hours and hours of work, and most of them provide a good insight into the documentation that has all the details of the package. Taking into account React’s modularity, it undoubtedly seems that using packages is second to none in this case.
More about the modern form handling process, their advantages and disadvantages, some advanced features in forms, as well as about which package is the most adequate at the moment, in my next blog.
In short, handling forms manually is quite a hard task. Have fun and stay tuned for more!
Join our newsletter
Like what you see? Why not put a ring on it. Or at least your name and e-mail.
Have a project on the horizon?