DEV Community

Cover image for React Form Patterns: Controlled vs. Uncontrolled Components
Masudur Rahman Sourav
Masudur Rahman Sourav

Posted on

React Form Patterns: Controlled vs. Uncontrolled Components

If React components were people, Controlled Components would be that friend who plans every minute of their vacation in an Excel spreadsheet. Uncontrolled Components would be the friend who shows up at the airport without a ticket, wearing flip-flops, saying, "The universe will provide."

Handling forms in React forces you to choose a side. Do you want to micro-manage every keystroke? Or do you want to let the DOM do its thing and just check the results later?

Let's break down the battle between Controlled and Uncontrolled forms.

🎮 The Controlled Component (The Micro-Manager)

In a Controlled setup, React is the single source of truth. The DOM input doesn't breathe without React's permission.

How it works:

  1. The User types 'A'.
  2. React says, "Hold on, let me update my State first."
  3. React updates State to 'A'.
  4. React tells the Input: "Okay, now you can display 'A'."

It’s a tight feedback loop. It’s secure. It’s predictable. It’s also a little exhausting.

🎨 The Graphical Explanation


    User Types 'H'
           |
           v
  +------------------+
  |  Event Handler   |  "Hey React, they typed H!"
  +--------+---------+
           |
           v
  +------------------+
  |   React State    |  "Updating state to 'H'..."
  +--------+---------+
           |
           v
  +------------------+
  |   Input Value    |  "Displaying 'H' as ordered, sir!"
  +------------------+

Enter fullscreen mode Exit fullscreen mode

💻 The Code

import React, { useState } from 'react';

const ControlFreakForm = () => {
  const [name, setName] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    // We can intercept! NO LOWERCASE ALLOWED! 👮‍♂️
    setName(value.toUpperCase());
  };

  return (
    <form>
      <label>Name (Screaming only):</label>
      <input 
        type="text" 
        value={name} 
        onChange={handleChange} 
      />
      <p>Current State: {name}</p>
    </form>
  );
};

Enter fullscreen mode Exit fullscreen mode

✅ Use Case: When you need instant validation (password strength meters), dynamic formatting (credit card spacing), or conditional disabling of the submit button.

🐎 The Uncontrolled Component (The "It Is What It Is")

In an Uncontrolled setup, the data lives in the DOM, not in React state. React is like a landlord who owns the building but doesn't have a key to your apartment. When React needs the data (like on submit), it has to knock on the door (using a ref) and ask, "Hey, what did you write in there?"

How it works:

  1. The User types 'A'.
  2. The Input displays 'A'.
  3. React is asleep.
  4. User hits "Submit".
  5. React wakes up and checks the Ref to see what happened.

🎨 The Graphical Explanation

      User Types 'H'
           |
           v
  +------------------+
  |   DOM Input      |  "I got this. I'll hold the 'H'."
  +------------------+
           |
           | (React ignores this part)
           |
      User Clicks Submit
           |
           v
  +------------------+
  |    React Ref     |  "Yo DOM, what's the value?"
  +------------------+


Enter fullscreen mode Exit fullscreen mode

💻 The Code

import React, { useRef } from 'react';

const WildWestForm = () => {
  const nameInputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    // Knocking on the DOM's door ✊
    alert(`Welcome, ${nameInputRef.current.value}!`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name (I don't care until you submit):</label>
      <input 
        type="text" 
        defaultValue="" 
        ref={nameInputRef} 
      />
      <button type="submit">Submit</button>
    </form>
  );
};

Enter fullscreen mode Exit fullscreen mode

✅ Use Case: When you are migrating legacy code, integrating with non-React libraries (like jQuery plugins... shudder), or building simple forms where you don't need instant validation.

🥊 The Showdown: Comparison

Feature Controlled Uncontrolled
Source of Truth React State The DOM
Data Access Instant (on every keystroke) On Demand (usually submit)
Validation Real-time (as you type) On Submit
Performance Re-renders on every keystroke Only re-renders on submit
Code Volume More boilerplate (handlers, state) Less boilerplate (refs)

🕳️ The Pitfalls (Gotchas)

1. The "File Input" Exception 📁

In React, an is always uncontrolled because its value can only be set by a user, not by code (security reasons). If you try to control a file input value, React will yell at you. Just use a Ref.

2. The Performance Myth 🐌

People say Controlled components are slower because they re-render on every keystroke. Unless you are pasting the entire script of The Bee Movie into a text area on a low-end Android phone from 2015, it usually doesn't matter. React is fast. Don't optimize prematurely.

3. The null vs undefined Trap 🪤

If you initialize a controlled input with value={undefined} (or null) and then switch it to a string later, React will scream: "A component is changing an uncontrolled input of type text to be controlled."
Fix: Always initialize state with an empty string useState('').

🏁 Conclusion

So, which one should you choose?

Controlled is the standard. It’s robust, "React-y," and allows you to do cool things like disable the submit button if the email is invalid. It’s high-maintenance but high-reward.

Uncontrolled is the quick-and-dirty. It’s great for quick hacks, simple forms, or when you just don't want to write a handleChange function for the 100th time today.

Top comments (0)