Creating forms in React
Introduction
Forms are essential components of the web, whether created with just HTML or with frameworks like React. Forms allow users to send data, perform basic tasks on the internet such as posting a tweet, sending a message, or online shopping, and also retrieve data. Forms are the web's foundation, and networking would not be possible without them.
Creating effectual forms is a necessity for developers because it allows them to collect data from users in a way that is efficient and easy to understand. When forms are well-designed, they can help developers avoid errors and ensure they are collecting the data they need. Additionally, effective forms can help improve the user experience by making it easy for users to provide the needed information.
Unlike HTML forms, React allows for the dynamic handling of form data. When working with forms in HTML or vanilla Javascript, it's essential to remember that the inputs can only be processed after submitting the form. This means that any changes made by the user won't be monitored until the very end of the process. However, in React, we have the ability to create states that can track every change made during the user's interaction with the form. This is a powerful tool that can significantly improve the user experience and make the process much more efficient. Information processing starts as soon as the user begins interacting with the form. This means that after submitting the form, we submit the data and pass the state that we have been tracking all along.
React presents two ways to handle data in forms: The first way is through uncontrolled components, which allow the form elements and data to be handled by the DOM. The second way is through controlled components, where the data is fully handled by React through hooks.
Prerequisites
Proficiency in HTML, particularly HTML forms and form elements, and CSS.
Knowledge of Javascript
Knowledge of the DOM
Basic knowledge of React and useState hook
A text editor for writing code
Creating Forms Through Uncontrolled Components
In uncontrolled components, the state of form elements is not managed by React, but rather by the DOM. When users input text into form fields in uncontrolled components, the browser controls the state of those elements and updates the DOM for that element.
Here, no value property is set for form elements, instead, we use the onChange
event handler to receive users' input. Moving on in this tutorial, we will:
Create basic forms
Manage the state of our input with the
useState
hook
Creating Simple Forms
First, create a Form.js file and import react
and useState
import React, { useState } from "react"
Then, export a default Form
function, returning a form
element within it
export default function Form () {
return (
<form>
</form>
)
}
Then, create an input
element inside the form with a type and placeholder of First name
and an onChange
event while passing in a handleChange
function. Like below
import React, { useState } from "react"
export default function Form () {
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
)
}
So, let's handle changes in the First name
input using the useState
hook. Let’s just create a handleChange
function and console.log
to check for changes in our input.
The handleChange
function must be nested inside the form component like below
import React, { useState } from "react"
export default function Form () {
function handleChange() {
console.log("Changed!")
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
)
}
So, we can test and see that as we type into our input field, Changed! is printed to the console
Next, we use let to declare a firstName
variable and console.log
firstName
to see the changes in the user’s input as they type
import React, { useState } from "react"
export default function Form () {
let firstName
console.log(firstName)
function handleChange() {
console.log("Changed!")
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
)
}
In our onClick
handler function, we pass event
to our function, then we remove the console.log
there, replacing it with the code
function handleChange(event) {
firstName = event.target.value
}
The code would appear as follows
import React, { useState } from "react"
export default function Form () {
let firstName
console.log(firstName)
function handleChange(event) {
firstName = event.target.value
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
)
}
Let's execute the file and see the output in the console
We can see that despite typing repeatedly, nothing happens in our console because React cannot detect changes in firstName
, so we use the useState
hook to declare our variable and set a state to allow for changes in firstName
.
First, we declare an array of firstName
and setFirstName
equal to useState
which is set to an empty string
const [firstName, setFirstName] = useState("")
Next, we pass an event object from the onClick
event handler to our handleClick
function, and then we replace the console.log
to update the firstName
state on every keystroke
function handleChange(event) {
setFirstName(event.target.value)
}
Then add a console.log in the body of the Form function to see the changes in user input as they type
import React, { useState } from "react"
export default function Form () {
const [firstName, setFirstName] = useState("")
console.log(firstName)
function handleChange(event) {
setFirstName(event.target.value)
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
)
}
We can see that, unlike the previous attempt, React is able to detect changes in its input and update firstName
accordingly.
Adding another input field to the form
We can add another input field to the form in two ways and we’ll explore both ways in this section
A. Duplicating Our Former Code By Adding Another Hook
By duplicating our former code and editing it accurately, we can add another input
field, and track and update its state in React as we formerly did.
First, we create another input
field with an onChange
event listener with a handleSurName
function
<input type="text" placeholder="surName" onchange={handleSurName} />
We create our surName
state variable with the useState
hook
const [surName, setSurName] = useState("")
The next step is to create the handleSurName
function using the handleChange
function as a model
import React, { useState } from "react"
export default function Form () {
const [firstName, setFirstName] = useState("")
const [surName, setSurName] = useState("")
function handleChange(event) {
setFirstName(event.target.value)
}
function handleSurName(event) {
setSurName(event.target.value)
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
<input type="text" placeholder="surName" onchange={handleSurName} />
</form>
)
}
Then console.log the firstName
and surName
to allow us to see the output
However, if we have more inputs, we will duplicate more code, leading to poor-quality code that may make it difficult to maintain. In the next section, we will learn how to do the same thing in a cleaner and more acceptable format.
B. Combining State into Object
By combining states, we can create and maintain a single state to manage multiple inputs. This allows us to use a single state to control and manage multiple inputs, checkboxes, and radio elements. It gives us greater control, simplifies the code, and improves code quality, especially if the code is to be used by other developers.
First, we the duplicate handleSurName
and state variable and replace the placeholder surName
with lastName
since that's easily understandable, and we pass handleChange
to it
import React, { useState } from "react"
export default function Form () {
const [firstName, setFirstName] = useState("")
function handleChange(event) {
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
<input type="text" placeholder="lastName" onchange={handleChange} />
</form>
)
}
To create a single state that can be used by all types of input, we need to create an object in the state. A state variable can accept multiple data types, so we create the object in the state. We need to change the name of the variable from firstName
to something more generic, like formFields
import React, { useState } from "react"
export default function Form () {
const [formFields, setFormFields] = useState({
firstName: "",
lastName: ""
})
function handleChange(event) {
}
return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
<input type="text" placeholder="lastName" onchange={handleChange} />
</form>
)
}
Unfortunately, we cannot use event.target.value
to get the data from the form fields because rather than giving us our data in object form, our object will be erased and the output will be a string, which is not helpful. Since we are using the same handleChange
function on both inputs, we need a way to identify which input triggered the event. To do this, we give the inputs a name
property that matches the property name that we are storing
<input
type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
/>
<input
type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
/>
To check if our code is working, we’ll console.log
event.target.name
to check which input field is triggering the event listener. Let’s test our code
With this, we have everything we need to call formFields
and update our object correctly. To do this, we call setFormFields
to accept changes in state and update our formFields
variable respectively. We’ll call setFormFields
in our onChange
function, and then we’ll pass our previous state (we didn’t do that with strings because overwriting our file doesn’t actually change anything) preventing outright overwriting of our earlier state, passing a callback function which will then return an object. The object takes in a copy of the previous state, then the specific property we want to change in the object is passed to it. This causes only the value of the property we want to change to be overwritten whilst preserving the other components of the object
import React, { useState } from "react"
export default function Form () {
const [formFields, setFormFields] = useState({
firstName: "",
lastName: ""
})
function handleChange(event) {
setFormFields(prevState => {
return {
...prevState,
[event.target.name]: event.target.value
}
})
}
return (
<form>
<input
type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
/>
<input
type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
/>
</form>
)
}
Computed properties was used to turn the dynamic string event.target.name
and use it as the property name for the object. Console.log
formFields
to see our state as it changes
The object is initially empty but as we start typing, the object starts changing and our state remains intact.
Adding Another Input
Adding another input field will be very easy since we just have to add a new field to our state object rather than adding another event handler function and a new state.
Let’s demonstrate that by adding a Username field in our state object. Kindly ensure that the name of the property in the state object is the same as the one in the input tag
import React, { useState } from "react"
export default function Form () {
const [formFields, setFormFields] = useState({
firstName: "",
lastName: "",
userName: ""
})
function handleChange(event) {
setFormFields(prevState => {
return {
...prevState,
[event.target.name]: event.target.value
}
})
}
return (
<form>
<input
type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
/>
<input
type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
/>
<input
type="text"
placeholder="Username"
onChange={handleChange}
name="userName"
/>
</form>
)
}
Let’s add favColour
to the state object
const [formFields, setFormFields] = useState({
firstName: "",
lastName: "",
userName: "",
description: "",
isTrue: true,
tiers: "",
favColour: ""
})
Then, we add a name
, value
and the onChange
event handler to the select
element
<select
id="favColour"
name="favColour"
value={formFields.favColour}
onChange={handleChange}
>
With those lines of code, the dropdown works as expected
However, the initial value of the select
element is an empty string, but there is no option that represents the empty string. We can resolve that by creating an option element with an empty string value
<option value="">-- Choose --</option>
And this is the total code
import React, { useState } from "react"
export default function Form () {
const [formFields, setFormFields] = useState({
firstName: "",
lastName: "",
userName: "",
description: "",
isTrue: true,
tiers: "",
favColour: ""
})
function handleChange(event) {
const {name, value, type, checked} = event.target
setFormFields(prevState => {
return {
...prevState,
[name]: type === "checkbox" ? checked : value
}
})
}
return (
<form>
<input
type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
value={formFields.firstName}
/>
<input
type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
value={formFields.lastName}
/>
<input
type="text"
placeholder="Username"
onChange={handleChange}
name="userName"
value={formFields.userName}
/>
<textarea
placeholder="description"
onChange={handleChange}
name="description"
value={formFields.description}
/>
<input
type="checkbox"
id="isTrue"
checked={formFields.isTrue}
onChange={handleChange}
name="isTrue"
/>
<label htmlFor="isTrue">Subscribing</label>
<select
id="favColour"
name="favColour"
value={formFields.favColour}
onChange={handleChange}
>
<option value="">-- Choose --</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
<option value="indigo">Indigo</option>
<option value="orange">Orange</option>
<option value="violet">Violet</option>
</select>
</form>
)
}
Submitting Forms
In React, the button
element is used to handle the submit. By default the button element is of type submit
, so we’ll use this in our code
<button>Submit</button>
Clicking the button will cause the form to trigger the onSubmit
event handler. So, we add the onSubmit
event handler prop to our form with the handleSubmit
function which we’ll create
<form onSubmit={handleSubmit}>
Next, create the handleSubmit
function and pass the event
to it. In the body of the function, run event.preventDefault
to prevent the page from refreshing, causing React to rerender the component
function handleSubmit(event) {
event.preventDefault()
}
This is the whole code, you can style it way you want to
import React, { useState } from "react"
export default function Form () {
const [formFields, setFormFields] = useState({
firstName: "",
lastName: "",
userName: "",
description: "",
isTrue: true,
tiers: "",
favColour: ""
})
function handleChange(event) {
const {name, value, type, checked} = event.target
setFormFields(prevState => {
return {
...prevState,
[name]: type === "checkbox" ? checked : value
}
})
}
function handleSubmit(event) {
event.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<input type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
value={formFields.firstName}
/>
<input type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
value={formFields.lastName}
/>
<input type="text"
placeholder="Username"
onChange={handleChange}
name="userName"
value={formFields.userName}
/>
<textarea
placeholder="description"
onChange={handleChange}
name="description"
value={formFields.description}
/>
<input
type="checkbox"
id="isTrue"
checked={formFields.isTrue}
onChange={handleChange}
name="isTrue"
/>
<label htmlFor="isTrue">Subscribing</label>
<br />
<fieldset>
<legend>Choose Subscription Plan</legend>
<input
type="radio"
id="tier1"
name="tiers"
value="tier1"
onChange={handleChange}
checked={formFields.tiers === "tier1"}
/>
<label htmlFor="tier1">Tier 1</label>
<br />
<input
type="radio"
id="tier2"
name="tiers"
value="tier2"
onChange={handleChange}
checked={formFields.tiers === "tier2"}
/>
<label htmlFor="tier2">Tier 2</label>
<br />
<input
type="radio"
id="tier3"
name="tiers"
value="tier3"
onChange={handleChange}
checked={formFields.tiers === "tier3"}
/>
<label htmlFor="tier3">Tier 3</label>
<br />
</fieldset>
<br />
<label htmlFor="favColour">What is your favourite colour</label>
<br />
<select
id="favColour"
name="favColour"
value={formFields.favColour}
onChange={handleChange}
>
<option value="">-- Choose --</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="yellow">Yellow</option>
<option value="indigo">Indigo</option>
<option value="orange">Orange</option>
<option value="violet">Violet</option>
</select>
<br />
<br />
<button>Submit</button>
</form>
)
}
With that, we’ve come to the end of this how-to guide.
Conclusion
Congratulations! You can now create forms in React. To access the source code used in this tutorial, head over to the repository to get it.
Subscribe to my newsletter
Read articles from Emmanuel Amoo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Emmanuel Amoo
Emmanuel Amoo
I'm Emmanuel, a full-stack developer and technical writer. I love solving diverse problems especially ones that challenge the norms and drive my creativity. I specialize in writing Node.js and React applications. My hobbies are designing, exploring art and reading.