React Forms - Uncontrolled Components

Chapter 24 14 mins

Learning outcomes:

  1. What are uncontrolled components
  2. When do we need uncontrolled components
  3. How to make uncontrolled components
  4. The defaultValue prop vs. value
  5. The defaultChecked prop vs. checked

What are uncontrolled components?

In the previous chapter, we learnt a great deal of information regarding controlled components in React and what exactly is meant by the term 'controlled' in there.

To review it quickly, a controlled component in React is a form input component whose data is controlled by React instead of the DOM itself.

As we enter anything into a controlled component, its value won't change unless our React code goes on and explicitly changes the value of the component (which is often done using the value prop).

Uncontrolled components are the opposite of controlled components.

An uncontrolled component in React represents a form input whose data is NOT controlled by React but rather by the DOM itself.

As we enter anything into an uncontrolled input, React can NOT step in and prevent the input elements from updating themselves unless explicitly provided a value.

The input elements are instead free to operate on their own will, just as they normally are. Thusly named — 'uncontrolled' components.

If we're coming from a vanilla JavaScript background, where we're used to directly dealing with input elements from the DOM, uncontrolled components would be the closest to this experience of ours.

In uncontrolled components, React doesn't have any control over the data entered into the components, and we directly get to programmatically query those components.

Now since React doesn't have control over the inputs when they are uncontrolled, and since we directly deal with them, we need to rely on refs in React in order to gain access to the actual DOM nodes of these uncontrolled input elements.

This is a customary practice when using uncontrolled components, which we shall see later on in this chapter as well.

When do we need uncontrolled components?

A common question popping up in the mind of any beginner React developer is that "Should I use controlled components or uncontrolled components?"

Or even that "When do we really need uncontrolled components?"

In this section, we shall answer both these questions, starting with the former.

So should we use controlled components or uncontrolled components when creating form input elements in React?

Well, this isn't any difficult to answer — clearly controlled components.

Controlled components allow us to involve React's core state utility into the equation and have access to input data directly inside of component functions. React itself encourages us to use controlled components to create form inputs.

Now over to the second question: When do we really need uncontrolled components, given that controlled components are the outright recommendation?

Recall that React is made in a way such that it can easily be plugged in existing apps. There is no need to rewrite entire apps in React before it could be leveraged within them.

In this regard, when transferring existing functionality, specifically in the case of form handling, to React, it might suit developers to temporarily rely on uncontrolled components so that they could reuse the functionality in their current form handling systems (written in JavaScript, and interacting with the DOM directly).

In other words, uncontrolled components make for an easy entry path into React when transitioning JavaScript codebases into it.

Coding controlled components is undoubtedly slightly complex; and as such, uncontrolled components offer less complexity and less entry barrier for bringing in existing functionality into React.

Long story short,

Uncontrolled components might only be needed to simplify certain form handling systems when transitioning them from vanilla JavaScript to React and not have to rewrite a lot of functionality (as is the case with controlled components).

However, if it's feasible for a developer, it's an outright recommendation to rely on controlled components and convert existing form handling systems to them, even if that means coding for a couple of extra lines.

How to make uncontrolled components?

Let's now see how exactly to make uncontrolled components in React. We've already seen certain parts to this in the previous chapter but let's see them now more clearly.

In order to make a form input component in React uncontrolled:

  • It should NOT have a value prop on it, or
  • The value prop should be null or undefined
Recall from the React JSX chapter that setting any attribute of a host component instance (i.e. a component representing a DOM element) to null or undefined is as good as removing the attribute from the component. Thus, the latter case above is simply an extension of the first case (of there being no value prop on a form input component).

When the value prop is not specified to a form input component, or when it's null/undefined, React will know not to interfere with its normal behavior.

Let's see a quick example.

In the following code, we have a text input element without any value prop on it, but with an onChange handler to log in the console every moment the input's data changes:

function App() {
   return (
      <>
         <p>Type anything below and then see the console:</p>
         <input type="text" onChange={e => console.log(e.target.value)} />
      </>
   );
}

Since the input element is void of a value prop, it isn't controlled and, thus, as we type into it, the entered data is shown right away.

In the link below, try interacting with the input field and side-by-side witnessing the logs made in the console.

Live Example

Now, let's say, we wish to take this simple program a bit further and add a button to validate the entered data to be not any longer than 3 characters. The moment the button is clicked, we should make an alert in the console: 'Valid' if this condition is met, or else 'Invalid'.

Simple enough of a task.

First, let's add the button in the JSX and remove the onChange handler from <input>, since we don't need it:

import { useRef } from 'react';

function App() {
   const inputRef = useRef();

   return (
      <>
         <input type="text" ref={inputRef} />
         <button>Check</button>
      </>
   );
}

Now, let's define the onClick handler on <button>:

import { useRef } from 'react';

function App() {
   const inputRef = useRef();

   function validateInput() {
      if (inputRef.current.value.length <= 3) {
         alert('Valid');
      }
      else {
         alert('Invalid');
      }
   }

   return (
      <>
         <input type="text" ref={inputRef} />
         <button onClick={validateInput}>Check</button>
      </>
   );
}

Upon the button's click, we ought to retrieve the value of the <input>, validate it against the given length requirement, and then finally make the desired alert.

Live Example

Notice how we're using a ref to obtain the value of the <input> element. As we learnt above, this is a customary routine when working with uncontrolled components in React — after all, there has to be some point where we interact with a DOM node representing a form input.

And this is pretty much how we'll interact with other input elements if they were present in our form — using refs to obtain their DOM nodes and then querying the DOM nodes directly.

To recap what we learnt in this section, in order to make a component uncontrolled, we need to have the value prop absent from it (or if present, have it set to null or undefined).

But what if we really want a given input element to start off with a particular value and keep it uncontrolled?

As we just learnt, adding the value prop ain't a feasible solution in this case because that'll make the component controlled, and we don't want that.

Now what? Well, this is something React realized long ago and provided features to work around it. These features are the defaultValue and defaultChecked props.

The defaultValue prop

The defaultValue prop is basically just the uncontrolled version of value. Period.

There's nothing really technical to learn about it.

If we wish to apply a value attribute to an input element in React but also wish to not transition it to a controlled component, then obviously we can't use the value prop; instead we need to use defaultValue.

Consider the following code where we have a text input element with an initial value to begin with. This value is set using the value prop:

function App() {
   return (
      <>
         <p>The following input has an initial value to begin with:</p>
         <input type="text" value="Initial value" />
      </>
   );
}

Expectedly, since the value prop is being used and there is nothing to update this value prop, our component would be read-only.

Live Example

But now, let's replace this value prop with defaultValue and see the effect produced:

function App() {
   return (
      <>
         <p>The following input has an initial value to begin with:</p>
         <input type="text" defaultValue="Initial value" />
      </>
   );
}

Open the linked page below and interact with the input therein.

Live Example

See how we're now able to enter anything into the input without it being passed over to React before being ultimately written inside the input element.

That is, using defaultValue, our input component has an initial value to begin with and it's also uncontrolled at the same time.

The defaultChecked prop

Just like the defaultValue prop is the uncontrolled complement of the value prop, the defaultChecked prop is the same for the checked prop.

As we already know, the value prop can't be applied to certain input elements, such as checkboxes and radios. In these cases, we use the checked prop to initially check them.

But this has the consequence of transitioning the inputs into controlled components. And that's an issue if we wish to keep the inputs uncontrolled.

The defaultChecked prop comes to the rescue here. It initially checks a checkbox or radio but doesn't make it controlled.

Below shown is an example of defaultChecked, contrasting it with the checked prop:

Following we have a checkbox with checked set:

function App() {
   return (
      <>
         <p>The following checkbox is initially checked:</p>
         <label>
            <input type="checkbox" checked /> Checkbox label
         </label>
      </>
   );
}

Live Example

As we click on the checkbox to uncheck it, there's no effect produced. And you know the reason by now — checked is set and that makes the input controlled.

Following we have the same checkbox albeit with defaultChecked instead of checked:

function App() {
   return (
      <>
         <p>The following checkbox is initially checked:</p>
         <label>
            <input type="checkbox" defaultChecked /> Checkbox label
         </label>
      </>
   );
}

Live Example

As we click on the checkbox to uncheck it now, we get the expected behavior, all thanks to the fact that the component is uncontrolled by virtue of there being no checked prop.

This was simple, what do you say?