How to Create a React Form: Controlled vs. Uncontrolled Components
In this article, we'll cover two ways to create React forms: the HTML way, with uncontrolled components, and the best practice, with controlled components.We'll discuss concepts such as: what are controlled and uncontrolled components, main tags used in a React form, and form validation and submission.
In HTML forms, data is handled by the DOM. In React, on the other hand, the best practice is to handle the data inside the components. This involves storing the data in the component’s state and controlling the changes with event handlers.
This may be hard to grasp in the beginning, as it requires you to think about form handling and validations a bit differently. However, it’s also more powerful and gives you more flexibility and control when working with more complex forms.
To help you understand the differences between the two methods, we’ll discuss both ways of handling forms in React: the HTML way, which uses “uncontrolled components”, and the best practice, with “controlled components”.
To make things easier to visualize, we’ll build the same landing page for the two forms.
WYWL: What You Will Learn#
By the end of this tutorial, you’ll be able to build a simple landing page with a functioning React form.
You can find the repository for this tutorial here.
Although this tutorial is for beginners and explains the main concepts from scratch, in order to follow through it’s best if you have some knowledge of:
If you're not familiar with these yet, please take a few minutes to go through the pages above before starting this tutorial.
Controlled vs. uncontrolled components in React#
In the browser, forms maintain their own internal state. The form data is handled by the DOM, so when you type something in an input field, for example, the data is "remembered" by the DOM.
To retrieve it, you need to "pull" the value of the field from the DOM, and this is usually done only once when the user hits the
submit button. This type of form is called "uncontrolled" because it relies on the DOM to manage the input data.
In React, we use a virtual DOM which no longer observes all the changes that happen in the DOM. The virtual DOM only observes what we tell it to, so if we want our form data to be “seen”, we need to tell React about the form changes.
Uncontrolled components in React are similar to HTML inputs, as they "remember" what you type in. Here's an example of a simple uncontrolled component.
If you want to access the data from uncontrolled components, you need to use a
Refs allow you to reference a DOM element or class component from within a parent component.
Refs allow you to “pull” the value from a field when you need it, so you can do it when the form is submitted, for example. This means less complex code for you, but it also means that you’re not able to handle validations in real-time.
Also, if you opt for uncontrolled components, you can’t disable the submit button for example and you can’t enforce a specific format for the text input as the user types something in.
All your validations will happen on submit, which can be quite annoying for the user, especially if you’re building longer forms with a lot of input fields.
However, uncontrolled components are the easiest way to create forms, and if you don’t really need to do much validation, you can safely use them.
With controlled components, you store the state in the component that renders the input, so in this case in the Form component.
Each input field accepts its current value as a prop and has a callback function which is called when the state of the input changes.
Here's an example of a controlled class component.
The callback function is controlled by the parent component - so the Form in our case. The new values of the state are passed to the input fields as props.
Since the value of the input field changes whenever you type a new character, so the state changes continuously.
This may sound bad but it’s actually useful, as the Form component is always “aware” of the state and the value of the input. Thus, if you want to validate something, you don’t need to “pull” the state value on submit and then display error messages.
For example, if you don’t want the e-mail address to include “@gmail”, you can display a validation message in real-time, and this contributes to a better user experience.
Step-by-Step Tutorial: React Form With Controlled Components#
Let’s start by initiating a new React app, using Create React App. I’ll use VSCode for this tutorial. First type this command in your terminal:
npx create-react-app form-demo
Then, move into the form-demo folder, and in the terminal, type
npm start to run the app in your browser.
Step 1: Create the App.js page layout#
npm install react-bootstrap in the terminal. Then, because this library doesn’t come with a predefined version of bootstrap, you’ll need to install bootstrap as well and add a CDN link in the
index.js file, like below:
npm install boostrap
Now in the
App.js file, add the following code to start creating the page layout:
If you save and reload the page, your app should now look like this:
Step 2: Create the
Form.js component in the
Let’s start building the form component. In the
src folder, create a new file called
Form.js and add the following code:
Next, replace the comment with the form fields:
At the moment this isn’t a controlled component, as we’re not using state or props. So let’s add that to the form. First, add the constructor right under the
Then, add the props to the form fields:
We’re doing two things here:
passing the value as a prop with
onChangeevent with the callback function that will update the state when the input value changes
So we now need to define the
onChange functions, and we’ll do this right under the constructor:
If you reload the page and add something in the name field, you’ll get this error:
That’s because we have to bind the
updateEmail functions in the constructor, like this:
So now if you refresh again and inspect the form, you should see the input value changing as you type:
Step 3: Add a checkbox for user consent#
Let’s add a checkbox to the form, to get the user’s consent for contacting them when this app launches.
To align the checkbox to the rest of the input fields, I’ve used an
offset of 2 columns.
Next, we need to initiate the value of this checkbox in the state object. We’ll assign it a boolean value, which will be false by default.
Now let's add a handler that will update the state on change.
Finally, we need to add a handler for submitting the form. The
handleSubmit callback function will be called when the user clicks on the
event.preventDefault() because we don't want to redirect the user to a new page when the form is submitted.
Let's not forget to bind this function in the constructor as well:
If you now reload the page and submit the form, you should see the alert message displayed:
Step 4: Validate the fields and show errors in real-time #
Before submitting the form, we want to validate the input fields to make sure they're not empty. This will happen purely on the client-side, so in the browser, not on the server-side.
By doing so, we can display error messages in real-time, to let the user know that the values added in the input fields aren't meeting the validation criteria.
To do the validation, we'll add another property to the state, called
touched, which will keep track of the fields that are changed by the user. Initially, none of the fields have been touched by the user, so there's no point to validate anything.
We'll therefore initiate both the
name and the
We want to know when the user interacts with these fields, so that we can run the validation function. So we need to add a new handler called
handleBlur, which will update the
touched values if the fields are touched:
So now every time the user interacts with the
Finally, let's make sure that the name field isn't empty, and that the e-mail address includes an "@" character.
We'll define a new function called
validateFields, as follows:
So we're first checking if the name field was touched. If it was, and the length of the string typed by the user is lower than 2, then we're displaying an error message.
Next, let's add the e-mail field validation and return the
This function needs to be invoked in the
render method, because we want to validate whenever there's a change in the input fields. So basically whenever the form is rendered again.
So let's add the validation function to the form, right under