React|React operation principles and state management

Controlled vs. Uncontrolled Components: The Secret to How React Hook Form Reduces Rendering

2
Controlled vs. Uncontrolled Components: The Secret to How React Hook Form Reduces Rendering

Chris is building a signup page. It’s a long form requiring an ID, password, address, and more.

However, every time he types, the screen feels slightly sluggish. Turning on 'Highlight updates' in the React Developer Tools, he sees that with every single character typed into the ID field, the entire form—and even the completely unrelated footer at the bottom—is flashing and re-rendering.

"I'm just typing text; why is the entire page repainting?"

This is a chronic performance issue inherent in the "Standard of React" known as the Controlled Component pattern. Today, we will explore the concept of Uncontrolled Components to solve this problem, and the principles behind React Hook Form, which utilizes them to drastically reduce rendering.

1. Controlled Components: React is King

This is the method we use most commonly. The Source of Truth for the input value is React's State.

// ControlledForm.tsx
import { useState } from 'react';

function ControlledForm() {
  const [value, setValue] = useState('');

  // 1. Every time a key is pressed (onChange)
  // 2. The State changes.
  // 3. Since State changed, the ENTIRE component re-renders. 💥
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}
  • Pros: You can perform validation or change the UI in real-time based on the input value (e.g., turning the border red if it exceeds 10 characters).
  • Cons: The entire component function executes again for every single letter typed. If the form is large, this causes severe performance degradation.
  • 2. Uncontrolled Components: DOM is King

    On the other hand, in Uncontrolled Components, React does not manage the input values. We leave them to the browser's DOM and simply "pull" the values out when needed (e.g., when clicking the submit button). We use ref for this.

    // UncontrolledForm.tsx
    import { useRef } from 'react';
    
    function UncontrolledForm() {
      const inputRef = useRef<HTMLInputElement>(null);
    
      const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        // Directly access the DOM to get the value only when needed.
        console.log(inputRef.current?.value);
      };
    
      // Typing does NOT trigger a re-render! (Because there is no State change)
      return (
        <form onSubmit={handleSubmit}>
          <input ref={inputRef} />
          <button>Submit</button>
        </form>
      );
    }
  • Pros: Even if you type, there are 0 re-renders. Performance is excellent.
  • Cons: It is tricky to implement immediate reactions (like real-time error messages) whenever the value changes.
  • 3. React Hook Form: The Best of Both Worlds

    "Can't we combine the performance of Uncontrolled Components with the usability of Controlled Components?"

    The library that started from this question is React Hook Form. Its core strategy is to "use uncontrolled components as the default, triggering renders only when state observation is absolutely necessary."

    Internal Working Principle

    The register function's job is simple. It connects a ref to that input and secretly registers onBlur or onChange event listeners.

    // RHFExample.tsx
    import { useForm, SubmitHandler } from 'react-hook-form';
    
    type Inputs = {
      example: string,
    };
    
    export default function App() {
      // Provides subscription features like watch, formState, etc.
      const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
      
      const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
    
      console.log("Render Check"); // Log does not appear while typing!
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          {/* register connects the ref internally */}
          <input {...register("example", { required: true })} />
    
          {/* Re-renders ONLY this part when an error occurs */}
          {errors.example && <span>This field is required.</span>}
          
          <button type="submit">Submit</button>
        </form>
      );
    }

    React Hook Form does not re-render the entire component even if the input value changes. It tracks values internally and updates only the necessary parts (Isolated Re-render) when validation fails or the form is submitted.

    4. When Should You Use Which?

    Chris now selects his tools based on the situation.

    ComparisonControlled Component (useState)Uncontrolled Component (Ref)React Hook Form
    Render FrequencyEvery keystroke (High)Almost never (Low)Only when necessary (Optimized)
    Data SourceReact StateDOMDOM + Internal State
    Recommended ScenarioWhen UI must change immediately based on input (Masking, Dynamic Fields)Very simple forms, integrating external librariesMost production forms, when performance is critical

    Key Takeaways

  • Controlled Component: React watches all input. Implementation is easy, but it causes many unnecessary re-renders.
  • Uncontrolled Component: Uses ref to store values in the DOM. Rendering performance is good, but real-time control is difficult.
  • React Hook Form: A "cheat code" library based on the Uncontrolled Component (ref) approach that minimizes rendering while making form validation and state management easy.

  • Now that we've mastered form inputs, it's time to learn how to share data between components. Let's compare global state management tools to escape the hell of Props Drilling.

    Continuing in: "[Global State] Context API vs. Zustand: Boilerplate-free Global State Management."

    🔗 References

  • React Docs - Controlled vs Uncontrolled Components
  • React Hook Form - Get Started
  • Performance Comparison