Introduction
In our last lesson on useRef, we discovered its core function: providing a mutable reference that persists across renders without causing them. A key application we explored was accessing DOM elements directly, which we saw in a login form example. This technique, where we let the DOM manage the input's state and use a ref to read it only when necessary, is the foundation of what's known as an uncontrolled component.
This lesson formalizes that concept. We will directly compare this approach to its counterpart, the controlled component, where React state dictates the input's value at all times. Our goal is to describe the difference between controlled and uncontrolled components and the trade-offs of each approach.
By the end of this lesson, you will understand:
- The fundamental difference in how data is managed (the "source of truth").
- The role of
useStatevs.useRefin each pattern. - The practical trade-offs concerning validation, performance, and predictability.
- Subtle, under-the-hood issues that can arise with controlled components.
Part 1: The Core Distinction: Who Owns the State?
At the heart of this topic is a simple question: for a given form input, where does its "state" or current value live? Is it held within the DOM node itself, or is it managed by React in a state variable? The answer determines whether the component is uncontrolled or controlled.
- Uncontrolled Component: The DOM is the "source of truth." The
<input>,<textarea>, or<select>element manages its own internal state. React is unaware of the changes until it explicitly asks for the value, typically via aref. - Controlled Component: React state is the "source of truth." The value of the input is driven by a state variable (e.g., from
useState). Every change to the input updates the state, which in turn causes the component to re-render and pass the new value back to the input.
To see these two patterns in action, the following video provides a concise, side-by-side code demonstration.
Controlled vs UnControlled Components In ReactJS | Interview Question
The video 'Controlled vs UnControlled Components In ReactJS' from the Rethinking UI channel clearly illustrates the implementation of both patterns.
Please watch the following two segments: Uncontrolled Component (00:46 - 02:30): Observe how useRef is attached to the input and how its value is only accessed inside the onSubmit handler. Controlled Component (02:30 - 03:56): Notice how useState is used to hold the input's value. Pay attention to the value and onChange props on the input element; they are the hallmarks of a controlled component.
The data flow for each pattern is fundamentally different. This is often the most helpful way to conceptualize the distinction.

Part 2: Analyzing the Trade-offs
Choosing between a controlled and an uncontrolled approach is a classic engineering trade-off. You are balancing control and predictability against simplicity and performance.
The React documentation frames this concept more broadly. A component is "controlled" when its important information is driven by props from a parent. An "uncontrolled" component manages its own state internally. While our focus is on form inputs, this principle applies to any component. A Panel component with an internal isOpen state is uncontrolled; a Panel that receives an isOpen prop is controlled by its parent.
Let's dive into the specific trade-offs for forms.
️ Controlled vs Uncontrolled Components in React – A Deep Dive
The article 'Controlled vs Uncontrolled Components in React – A Deep Dive' by Fazal Mansuri provides an excellent breakdown of the pros, cons, and key differences.
Please read the first three sections of the article: 'What Are Controlled Components?': Focus on the pros and cons listed. 'What Are Uncontrolled Components?': Again, focus on the pros and cons. 'Controlled vs Uncontrolled: Key Differences': Study the summary table, which provides a concise comparison.
Let's synthesize what we've learned from the video and the article into a summary of the trade-offs:
| Feature | Controlled Components | Uncontrolled Components |
|---|---|---|
| Source of Truth | React State (useState) |
The DOM |
| Data Flow | Two-way binding (declarative). State drives the UI. UI updates the state. | One-way data retrieval (imperative). React pulls data from the DOM when needed. |
| Validation | Easy and Instant. You can validate or transform input on every keystroke inside the onChange handler. |
Delayed and Manual. Validation typically happens only on form submission. |
| Performance | Can be an issue. Causes a re-render on every single keystroke, which can be costly in large or complex forms. | Generally better. No re-renders on input changes. Ideal for simple forms or performance-critical areas. |
| Predictability | High. The component's value is always in sync with the React state, making it a "single source of truth." | Lower. The true value lives in the DOM, outside of React's state management. |
| Use Cases | Complex forms, fields that need instant validation/feedback (e.g., character count), inputs that affect other UI elements. | Simple forms (e.g., login, search), file inputs, or when integrating with non-React DOM libraries. |
React's official recommendation generally favors controlled components because they align better with the declarative nature of the library. Having the form data handled by the React component makes the state more predictable and easier to manage.
Part 3: Deep Dive into Controlled Component Edge Cases
Your interest in the underlying mechanics is particularly relevant here. While controlled components are generally preferred, their implementation—where React hijacks the native input behavior—can lead to subtle but frustrating bugs.
The same article we just reviewed explores two excellent examples of this.
️ Controlled vs Uncontrolled Components in React – A Deep Dive
Let's return to the 'Deep Dive' article to explore some non-obvious consequences of the controlled component pattern.
Please read the following two sections: 'Valtio & Controlled Inputs: Why Cursor Jumps Happen': This explains a common issue with state management libraries that batch updates. Focus on the why—the delay between the user's action and the re-render causing the browser to lose the cursor's context. 'Undo/Redo... in Controlled Components': This section explains why native undo/redo (Cmd+Z) can break. Understand that by setting the value programmatically on every keystroke, React is interfering with the browser's own input history stack.
Let's break down these two issues, as they reveal a lot about the browser and React's interaction:
-
Cursor Jumping: When you type in the middle of an input, the browser natively handles keeping the cursor in the right place. In a controlled component, the flow is:
keystroke -> onChange -> setState -> re-render -> value prop is updated. IfsetStateis batched or asynchronous (which is common, especially with libraries like Redux or Valtio), there's a tiny delay. In that moment, React might re-render the entire input value. The browser, seeing a new value being programmatically set, loses its internal reference to the cursor's last position and defaults to placing it at the end. This is a classic conflict between the imperative browser DOM and React's declarative updates. -
Broken Undo/Redo: The browser maintains a history stack for form inputs to enable undo/redo functionality. This stack is built on native user interactions. When a controlled component updates the
valueprop on every render, it's essentially telling the browser, "Forget what the user just did; here is the new value." From the browser's perspective, this isn't an incremental user change to be added to the undo stack. It's a programmatic replacement. As a result, the undo history is often wiped out, breakingCmd+Z. In scenarios where rich text editing is crucial, this is a strong argument for using an uncontrolled component or a specialized library.
These edge cases are perfect examples of why understanding the "how" is critical. They are not React bugs, but rather inherent consequences of the controlled component pattern's architecture.
Conclusion
We've dissected the two primary patterns for handling form data in React. The choice is not about right vs. wrong, but about making an informed decision based on the specific requirements of your component.
Key Takeaways:
- Controlled vs. Uncontrolled: The core difference is the "source of truth." In controlled components, it's React state; in uncontrolled components, it's the DOM.
- Implementation: Controlled components use
useState,value, andonChange. Uncontrolled components useuseRefto pull data imperatively. - The Trade-off:
- Choose controlled for predictability, instant validation, and when the input's state needs to be shared or coordinated. This is the default React way.
- Choose uncontrolled for simplicity, performance in high-frequency update scenarios, handling file inputs, or when preserving native browser behavior (like undo/redo) is critical.
- Under the Hood: The controlled pattern, while powerful, works by overriding native browser behavior, which can lead to subtle edge cases like cursor jumping and broken undo stacks.
What's Next?
In this lesson and the previous ones, we've seen how hooks like useState, useEffect, and useRef are used to manage logic within a component. Often, this logic becomes complex and repetitive. For example, you might have multiple controlled inputs that all need state, an onChange handler, and validation logic.
In our next lesson, we will learn how to implement a custom hook to encapsulate and reuse stateful logic between components. This is a powerful pattern that allows you to create your own hooks, like a useFormInput hook, to build cleaner, more maintainable, and highly reusable components.