With any application, handling forms is a crucial part as we need to allow users to input and submit various information for processing. The normal way of dealing with forms in HTML such as using text inputs, checkboxes, selects, radio buttons, and so on remains true even with React.Js.
In React, you can either choose to allow the browser to handle most of the form elements and collect data through React change events, or you can use React to fully control the elements by setting and updating the input value directly. The first approach is called an uncontrolled component because React is not setting the value. The second approach is called a controlled component because React is actively updating the input.
In this article, we will check the two ways of handling forms in a React.Js application, how to submit a form (e.g. callback handler), and how to reset a form (e.g. after submission). By the end of this tutorial, youâll be able to make a variety of forms using text inputs, checkboxes, select lists, and more.
Prerequisite
Have a development environment running Node.js
A React development environment set up with Create React App.
Basic knowledge of React such as how to manage state using
useState
, how to create components, etc.You will also need a basic knowledge of JavaScript and HTML. We will also be using Tailwind CSS for our styling and basic knowledge of the framework would be beneficial. However, you don't need to know Tailwind CSS as all the styling is provided.
Click Here to get the starter code. The code to the complete demo used in this article is provided at the end of the article.
REACT FORM BY EXAMPLE
A common example of a form in various web applications is a login form where we capture both the email and password of the user we want to authenticate. A functional representation of such a form is as shown below
import React from "react";
const Form = () => {
return (
<form className="form">
<div className="form-group">
<label htmlFor="email">Email</label>
<input className="form-input" type="email" />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input className="form-input" type="password" />
</div>
<button className="form-button">Submit</button>
</form>
);
};
export default Form;
If you click submit button, the form will reload. This is the default behavior of a form that we should prevent in our form. We need to handle the submit
event inside the component
To handle the event, weâll add an event handler to the <form>
element, not the <button>
. Create a function called handleSubmit
that will take the SyntheticEvent
as an argument. TheSyntheticEvent
is a wrapper around the standard Event
object and contains the same interface. Call .preventDefault
to stop the page from submitting the form then trigger an alert
to show that the form was submitted:
import React from "react";
const Form = () => {
const handleSubmit = (event) => {
event.preventDefault();
alert("You have submitted the form.");
};
return (
<form className="form" onSubmit={handleSubmit}>
<h4>Login</h4>
<div className="form-group">
<label className="form-label">
Email
</label>
<input className="form-input" type="email" />
</div>
<div className="form-group">
<label className="form-label">
Password
</label>
<input className="form-input" type="password" />
</div>
<button className="form-button">Submit</button>
</form>
);
};
export default Form;
Collecting Form Data Using Controlled Components
In the form above, we are rendering a simple form having two inputs one for email and the other for password. Nothing special. It is just like our regular HTML input. But to handle this input in React, we will need to understand the concept of a controlled input.
Uncontrolled and Controlled Input
In a React application, an "uncontrolled input" is an input field that is not associated with a React component state. This means that the value of the input is not managed by the React component, and the component does not have direct access to the value of the input.
On the other hand, a "controlled input" is an input field that is associated with a React component state. The value of the input is managed by the React component, and the component has direct access to the value of the input. This means that the component can programmatically control the value of the input, as well as respond to changes in the input value.
We will use the controlled way to handle the input's state. The benefit of using controlled input is that we are making the component state the single source of truth for the inputs. All we have to do is declare a state
object where data will live.
const [email, setEmail] = useState("") // Set default value to ""
Now, for us to make the input field a controlled input, we assigned the state variable (which contains a default empty string) to the value
prop.
<input className="form-input" value={email} type="text" />
After adding the value prop to input and assigning it to the email value from the state, if you try to write anything in the email text field, nothing will happen. This is because the value
prop is assigned a state variable whose value is set to an empty string. And this is being forced on the input.
This is good because we now have total control over the input state. Letâs go ahead and update it. Update the code to include an onChange
event handler.
const handleChange = e => {
setEmail(e.target.value)
}
...
<input className="form-input" value={email} onChange={handleChange} id="email" type="text" />
React needs an onChange
handler to keep track of any changes in the field. Anytime you write something in the input field, this onChange
event will trigger and then call its handleChange
function that will re-render the state using setEmail
function.
At this point, we have a controlled email input field where its state is being managed by its component.
Handling Multiple Inputs
In reality, youâll be working with multiple input fields in your React application. Our example form has two inputs for email and password. In this scenario, we will make a simple adjustment not only to the handler function but also to the input
element and the state.
We could decide to set up another useState
Hook for the password input. Then go ahead and assign its state variable to the value
prop. But this approach will require us to define another handler function to update the input state.
When we need to handle multiple inputs, we donât want to make a new onChange handler function for each input. Therefore, we want to make a function that can set all values.
We can do that as follows:
const [state, setState] = useState({
email: "",
password: "",
})
const handleChange = (e)=> {
setState({
...state,
[e.target.name]: e.target.value
});
}
NB: Our form elements must have a name property with a value matching the key for the elements state. For example, the email input must have name="email"
since we are using the key email
on the state to store the value for that input and the password input must have name="password"
.
Our input will change to be:
...
# Email Input
<input
className="form-input"
type="email"
name="email"
value={state.email}
onChange={handleChange}
/>
...
# Password Input
<input
className="form-input"
type="password"
name="password"
value={state.password}
onChange={handleChange}
/>
What is happening?
First, you will notice a significant change in the code. We started by modifying the useState
Hook to include an additional input data. From there, we have access to the email and password through state.email
and state.password
as used in the value
prop of their respective input
element.
In these input
elements, weâve added a name
prop that holds also their respective state name (i.e email
and password
). This is very important.
import React, { useState } from "react";
const Form = () => {
const [state, setState] = useState({
email: "",
password: "",
});
const handleChange = (e) => {
setState((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
alert("You have submitted the form.");
};
return (
<form className="form" onSubmit={handleSubmit}>
<h4>Login</h4>
<div className="form-group">
<label className="form-label">
Email
</label>
<input
className="form-input"
type="email"
name="email"
value={state.email}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label className="form-label">
Password
</label>
<input
className="form-input"
type="password"
name="password"
value={state.password}
onChange={handleChange}
/>
</div>
<button className="form-button">Submit</button>
</form>
);
};
export default Form;
Now, letâs focus on the handleChange
function. Here, we are using the setState
function to update the inputs state.
const handleChange = (e)=> {
setState({
...state,
[e.target.name]: e.target.value
});
}
In this function, we are simply assigning to the element that is being targeted (through [
e.target.name
]
) their corresponding values.
Still on the handleChange
function. Anytime we group related data as we have it in the state variable, the state returned by the useState
hook is not merged with that of the update passed to it. In other words, the useState
Hook doesnât merge the old and new state. Instead, it overrides the entire state with that of the current. So to avoid this scenario, we merge them by spreading the entire state object using the three dots before the state and overriding the part of it.
Now that you know how the control field works in React, adding any other input fields to a form will be a piece of cake. We need to update the state object to include the property for the new input, add the input, eg textarea
to our form and add the name, value, and onChange properties to it similar to how we added them to the email and password input. Let's check some examples of other inputs.
The TextArea Input Field
In React, the textarea
is defined as a self-closing element just like the input
element.
As expected, we will have the state manage the userâs input (i.e textarea message). So, update the state to include a message
property like so:
const [state, setState] = useState({
email: "",
password: "",
message: ""
});
Next, add a textarea
element in the form
like so:
...
return (
<form className="form" onSubmit={handleSubmit}>
...
<div className="form-group">
<label className="form-label">
Message
</label>
<textarea
className="form-input"
name="message"
value={state.message}
onChange={handleChange}
/>
</div>
<button className="form-button">Submit</button>
</form>
)
Take note of the value
and name
prop in the textarea
element. Just like the input field, the string assigned to the name
prop must be the same as what we declared in the state object.
The Select Input Field
This is not different from the other input fields. As usual, we can make it a controlled input by first having the state manage the input data. Then add a value
prop to the element and finally update it through the onChange
handler function (but in our case, we donât have to do anything here because we have the logic set already).
And donât forget to add a name
prop (to the element) that matches the name in the state. So letâs create a dropdown list with options to select car brands.
As expected, add a new property in the state. In my case, I will call it carBrand
.
const [state, setState] = useState({
...
carBrand: "",
});
Then, add the select
element just before the closing </form>
tag:
...
return (
<form className="form" onSubmit={handleSubmit}>
...
<div className="form-group">
<label className="form-label">
Favorite Car Brand
</label>
<select
className="form-input"
name="carBrand"
value={state.carBrand}
onChange={handleChange}
>
<option value="mercedes">Mercedes</option>
<option value="bmw">BMW</option>
<option value="audi">Audi</option>
</select>
</div>
<button className="form-button">Submit</button>
</form>
)
The Checkbox Input Field
Unlike the other input fields, the checkbox uses a checked
prop (which is a Boolean attribute) instead of the value
prop. The idea is that a checkbox is either checked or not.
We will need to adjust the handler function to accommodate the checkbox type of input. Ok, letâs start by adding a new property to the state. We can call it isChecked
.
const [state, setState] = useState({
...
isChecked: false,
});
Here, we assign a Boolean value of false
so that the input field is unchecked by default. Next, add input checkbox just before the closing </form>
tag.
...
return (
<form className="form" onSubmit={handleSubmit}>
...
<div className="form-group">
<label className="form-label">
<input
type="checkbox"
className="form-input"
name="isChecked"
checked={state.isChecked}
onChange={handleChange}
/>
Remember Me?
</label>
</div>
<button className="form-button">Submit</button>
</form>
)
Finally, update the handleChange
function so you have:
const handleChange = e => {
const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
setState({
...state,
[e.target.name]: value
})
}
In this function, we cannot use the earlier logic to manage the checkbox because it doesnât have the value
but checked
attribute. So youâd need to adjust it if you want the same handleChange
to manage the checkbox.
As seen in the handler, we now target the type
and the checked
attribute from this event parameter, e
. From there, we are using the ternary operator, which is an inline if-statement to check the input types and then assign their corresponding value (either Boolean e.target
.checked
for the checkbox or e.target
.value
for every other input types).
The Radio Input Field
The radio input types combine the input text and the checkbox type. In other words, they use both the value
and the checked
prop.
We will create radio inputs that allow users to select gender. As expected, letâs add that to the state.
const [state, setState] = useState({
...
gender: "",
});
Then, add the radio
inputs just before the closing </form>
tag:
...
return (
<form className="form" onSubmit={handleSubmit}>
...
<div className="form-group">
<label className="form-label">
<input
type="radio"
className="form-input"
name="gender"
value="male"
checked={state.gender === "male"}
onChange={handleChange}
/>
Male
</label>
<label className="form-label">
<input
type="radio"
className="form-input"
name="gender"
value="female"
checked={state.gender === "female"}
onChange={handleChange}
/>
Female
</label>
</div>
<button className="form-button">Submit</button>
</form>
)
Whatâs happening?
As you know already, once you have the state manage your input, you immediately assign the state property to the name
prop of the input. You should know from HTML that radio group share the same name. This allows us to select only one button at a time.
Notice that the value
prop in these inputs are static unlike that of text inputs where its value comes from the state.
And finally, with the checked
prop, we are saying that if the condition assigned is true
, that radio button should be checked.
Simple as that!
While working with inputs in a React application, these are the most popular and all that we cover in this article.
Conclusion
We should handle input changes by writing controlled components. To do this, we attach an event handler function to the onChange
event.
To handle form submission, we attach an onSubmit
event handler to the form, and then get the event
an object from the parameter and call event.preventDefault
inside so we can run JavaScript code in the handler.
To handle multiple input changes with one onChange
handler, we get the name
and value
properties from event.target
from the handlerâs parameter and update it with the dynamic property name feature available since ES6.
Source Code => https://github.com/marville001/form-handling-react
Be on the lookout for part 2 of this blog covering form validation using libraries such as react-hook-form and formik.
That's all for this article.
Thank you :)