DEV Community

Cover image for ⚛️ Controlled vs Uncontrolled Components in React – A Deep Dive
Fazal Mansuri
Fazal Mansuri

Posted on

⚛️ Controlled vs Uncontrolled Components in React – A Deep Dive

When building forms in React, you’ll inevitably come across controlled and uncontrolled components.
At first glance, they look similar — both accept user input and manage form data.
But how they manage that data under the hood can significantly impact performance, maintainability and UX.

In this blog, let’s break down the difference, see them in action, explore real-world issues (like cursor position and undo/redo bugs), and understand when to use each.


🧠 What Are Controlled Components?

In controlled components, React state is the single source of truth.
That means every keystroke or change updates the component’s state and that state determines what’s rendered.

Example:

import { useState } from "react";

function ControlledInput() {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="Type your name"
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, the <input> value is fully controlled by React via the name state.
Each keystroke triggers setName(), re-rendering the component with the updated value.

Pros

  • Easy to validate or transform input on change.
  • Centralized state makes debugging and unit testing easier.
  • Integrates seamlessly with complex UIs or form libraries (Formik, React Hook Form).

⚠️ Cons

  • Frequent re-renders can impact performance in large forms.
  • Need to carefully manage performance and input lag.
  • Cursor position bugs can occur with advanced state management.

🧩 What Are Uncontrolled Components?

In uncontrolled components, the DOM maintains its own state — React just references it.
You don’t handle every keystroke; instead, you access the value when needed.

Example:

import { useRef } from "react";

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

  const handleSubmit = () => {
    alert(`Input value: ${inputRef.current.value}`);
  };

  return (
    <>
      <input ref={inputRef} placeholder="Type your name" />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pros

  • Better performance for simple forms.
  • Minimal React re-renders.
  • Perfect when you just need the final value.

⚠️ Cons

  • Harder to validate or modify values dynamically.
  • Not ideal when input data must always reflect application state.

⚔️ Controlled vs Uncontrolled: Key Differences

Feature Controlled Uncontrolled
Data Source React State DOM (via refs)
Performance May re-render often Faster for simple inputs
Validation Easy to handle dynamically Requires manual check
Use Case Complex, validated forms Simple or performance-critical inputs

⚡ Valtio & Controlled Inputs: Why Cursor Jumps Happen

When you use a state library like Valtio in a React controlled input, you might encounter this issue: you edit text in the middle of the input, but the cursor jumps to the end. This frustrates users and breaks the typing experience.

🔍 What’s going on?

By default, Valtio batches updates before triggering re-renders. The documentation states:

“By default, state mutations are batched before triggering re-render. Sometimes, we want to disable the batching.”
Because of this batching behavior, when you type and update the proxy state, multiple changes may be queued and then re-rendered in one go. During that delay, the browser loses context of the input cursor position, so after render the input’s value is applied and the cursor jumps to the end.

✅ The fix: { sync: true }

Valtio provides the option to disable batching for specific use-cases like inputs:

const snap = useSnapshot(state, { sync: true });

With sync: true, updates go through immediately rather than being deferred/batched. This keeps the input cursor where the user expects it.

⚠️ Trade-off:

Using sync: true disables batching optimizations, which can reduce render efficiency in large/complex components. Therefore:

  • Use sync: true only in input fields where cursor behaviour matters.
  • Avoid it for general state where performance is more important than instantaneous update.

🔁 Undo/Redo (Cmd+Z / Cmd+Shift+Z) in Controlled Components

Another subtle but important issue:
Undo (Cmd+Z) and Redo (Cmd+Shift+Z) might not work as expected in controlled inputs.

Why?
Because React replaces the entire value on every keystroke rather than letting the browser handle native input history.

This means the browser sometimes loses the input undo stack, especially if re-renders happen between updates.

🧪 What to do:

  • Test undo/redo behavior in all input fields before handing over to QA.
  • If using a state library or custom hooks, check whether it maintains the input history properly.
  • In cases where it fails and browser-native undo is critical (e.g., long text editors), consider using uncontrolled inputs or controlled wrappers that debounce updates.

🧰 Best Practices for React Forms

✅ Use controlled inputs when you need validation, dynamic control or shared state.
✅ Use uncontrolled inputs when performance and simplicity matter.
✅ For state libraries like Valtio, test cursor and undo behaviors thoroughly.
✅ Avoid unnecessary re-renders – use React.memo, useCallback or split components.
✅ Always test form UX — typing, cursor and undo/redo should feel native and fluid.


🎯 Final Thoughts

Controlled vs Uncontrolled Components seem like a small React concept, but they can deeply impact user experience, especially in complex form-heavy applications.

💡 Understanding these nuances (like cursor handling, undo/redo, and batching) helps you ship smoother, bug-free UIs that feel professional and reliable.

🚀 Whether you use plain React or libraries like Valtio, always test the real typing experience — that’s where users feel the difference.

💬 Have you ever faced cursor or undo issues in your React forms? Drop your experience or workaround below — let’s learn together! 👇

Top comments (0)