React Hook Form Handling Basics: Uncontrolled and Controlled Components

A short post on what is Uncontrolled and Controlled components. How we use them with React hooks for form handling? Why and when to choose one over the other?

I have been helping some junior devs learn React at work and it has been a great refresher for me as well. We all learn new language, frameworks or libraries from time to time for various reasons but getting to see people new to programming learn a library or framework is a totally different thing. A lot of terms, concepts or principles that immediately make sense to us may seem alien to them. I think mentoring people new to programming makes us uncover new pain points and gotchas. We should all do that whenever we can to better understand how we can make it easy for others to join us on this side. Today, I will be talking about one such topic that makes a lot of sense after you know it but may not be the easiest to grasp if you are new to it all.

Let’s get started…

Controlled and Uncontrolled components are basically two ways of handling form input in React. In this tutorial, we will create a small app that will have two independent forms - one implemented using Controlled components while the other using Uncontrolled components. To keep it really minimal, each of the forms will have only one text input. Finally, on the submission of either form, we will update a central state lifted to a common parent component used to display the values submitted by each form. This part is included to emulate that the forms are part of a bigger application and the data entered through the form is used elsewhere in the app as well (like any real-world application).

This is how the final app will look.

Uncontrolled Components

We will start with the Uncontrolled components because it is the more “vanilla” of the two. To understand why I used the word “vanilla” here let’s take a look at how exactly a form element usually works. In case of an HTML text <input>, the user types something into it which the browser responds to by updating the value of the input which displayed to the user as the input element’s content. It is more or less the same thing with other input types like <select> where instead of typing the user chooses an option out of all the available ones. But in all the cases, the browser keeps track of the current value of a form elements i.e. the state of the form element. Uncontrolled components take advantage of the fact that the browser keeps track of the form element states by default and on form submission accesses the current value from the DOM elements itself.

Let’s see how we can build this using React hooks.

import React, { useRef } from "react";

export const Uncontrolled = props => {
const inputRef = useRef(null);

const handleSubmit = event => {
event.preventDefault();

const $inputDOMNode = inputRef.current; if ($inputDOMNode && $inputDOMNode.value) { props.handleSubmittedData({ uncontrolled:$inputDOMNode.value });
}
};

return (
<form onSubmit={handleSubmit}>
<h2>Uncontrolled</h2>
<input
name="uncontrolled"
placeholder="uncontrolled"
type="text"
ref={inputRef}
/>
<button type="submit">Submit</button>
</form>
);
};


As you can see here, we don’t have any state variable in this component. We have a ref inputRef that is used to grab hold of the input DOM element. Then, all we have is a handleSubmit function passed to the onSubmit handler prop on the form to handle the form submission. In this function, we grab the current value of the input using the ref we registered earlier and pass the value to the parent component using props.handleSubmittedData.

Controlled Components

Now let’s look at the Controlled components, the more commonly used form handling pattern in real-world React apps. We have already seen how browsers keep track of the form elements’ state by using the value attribute. But in controlled components, React is in charge of the form state. The value of a form element is stored in a state variable and is updated using a change handler registered using the onChange prop on the DOM element. The updated state variable then causes the value attribute on the DOM element to get updated.

Let’s see how we can build this using React hooks.

import React, { useState } from "react";

export const Controlled = props => {
const [controlledValue, updateControlledValue] = useState("");

const handleChange = event => {
updateControlledValue(event.target.value);
};

const handleSubmit = event => {
event.preventDefault();

props.handleSubmittedData({
controlled: controlledValue
});
};

return (
<form onSubmit={handleSubmit}>
<h2>Controlled</h2>
<input
name="controlled"
placeholder="controlled"
type="text"
value={controlledValue}
onChange={handleChange}
/>
<button disabled={!controlledValue} type="submit">
Submit
</button>
</form>
);
};


‌Let’s break the implementation down - we use controlledValue state variable to track the value and the handleChange function as the onChange handler prop of the <input> element. Inside this function, we update the controlledValue state.

For our handleSubmit implementation, we take the current value of the state variable used for tracking the input element state and pass it to the parent component using props.handleSubmittedData.

One additional thing you might notice here is that we do a boolean check !controlledValue to disable form submission when the input value is falsy like an empty string. This is to emulate basic validation.

This was just a brief introduction to form handling basics in React. There is no winner pattern here between the two as is almost always with most things in software. There are pros/cons to both the approaches and are both equally useful. But before we wrap up, I will list down some key points about each of them.

Controlled Components

Uncontrolled Components

Idiomatic

Seems more idiomatic and is sort of the recommended way of handling forms in React

It is more of an imperative way of handling forms and hence does not seem very “Reacty”

Ease of use

You need to track the state of each element and have change event handlers handle all possible state changes

You don’t need any state tracking, but you do need access to the DOM nodes of the form elements (the recommended way is using Ref)

Validation / Dynamic Forms

It is easier to build validation and dynamic forms in this way, because you have access to the current form state at all times that can used to conditionally render / disable / alter other parts of the app. Like we did with the disabled prop in the submit button

For any kind of side effects to be run like validation / any dynamic rendering based on the form state, you need to first grab hold of the current values of each of the form elements because they are not tracked in state automatically which can be a hassle

Performance / Scale

Performance usually isn’t much of an issue unless it is a very large and complex form with a lot of state updates. Memoization and other tricks may help in that case.

It is usually the more performant option because there is no state tracking in React and hence no re-renders happening on form input interactions. But managing Ref handles can get tricky in large forms

A concise side-by-side comparison of both the patterns (not exhaustive by any means)

Lastly, when building a real world application (most of which tend to have non-trivial forms in them), you are most likely to reach for one of the amazing packages on npm that allow easier handling of forms in React. There is a great selection of packages out there that all offer mature and battle-tested solutions. Some of the ones I personally have used and like to recommend to others are -

All three of them take different approaches to the problem. Go take a look at them and see if you can recognize which of the patterns they are using and how.