Dev Logs
/React/ Controlled vs Uncontrolled Inputs: Form Mastery
Chapters
  • 01Props & Children: Component Communication Mastery
  • 02useState & Functional State: Making Components Interactive
  • 03Rendering Logic, Lists, and Keys: Dynamic Content Mastery
  • 04Handling DOM Events: User Interactions Mastery
  • 05Controlled vs Uncontrolled Inputs: Form Mastery
    • What You'll Learn
    • Controlled Components: React Takes Control
    • Understanding Controlled Components
    • Advanced Controlled Component Patterns
    • Uncontrolled Components: Let the DOM Handle It
    • Understanding Uncontrolled Components
    • Advanced Uncontrolled Patterns
    • Controlled vs Uncontrolled: When to Use Each
    • Decision Matrix
    • Performance Comparison
    • Hybrid Approaches
    • Mixing Controlled and Uncontrolled
    • Converting Between Patterns
    • Common Mistakes & Anti-Patterns
    • . Mixing value and defaultValue
    • . Forgetting to handle null/undefined values
    • . Inefficient controlled components
    • Mini Challenges
    • Challenge 1: Smart Form Component
    • Challenge 2: Form State Manager Hook
    • When and Why: Decision Framework
    • Quick Decision Tree
    • Performance Guidelines
    • Interview Insights
    • Common Interview Questions
    • Code Review Red Flags
    • Best Practices to Mention
    • Key Takeaways
    • Decision Matrix Summary
    • Mental Model
    • Production Tips
  • 06useEffect: Dependencies & Cleanup Mastery
  • 07useRef: Refs vs Variables Deep Dive
  • 08useCallback: Function Memoization
  • 09useContext: Global State Management
  • 10useReducer: Complex State Management
  • 11. Custom Hooks: Reusable Logic 🎣
  • 12. React.memo & Optimization 🚀
  • 13. Advanced Patterns 🎨
  • 14. Error Boundaries & Error Handling 🛡️
  • 15. Testing React Applications 🧪
  • 16. Performance Optimization ⚡
  • 17. Deployment & Production 🚀
All chapters

🎛️ Controlled vs Uncontrolled Inputs: Form Mastery

Master React's input patterns: When to control state, when to let the DOM handle it, and how to choose the right approach for your forms

🎯 What You'll Learn

  • The fundamental difference between controlled and uncontrolled components
  • When and why to use each pattern
  • Form validation strategies for both approaches
  • Performance implications and optimization techniques
  • Hybrid approaches and advanced patterns
  • Real-world form handling scenarios
  • Common pitfalls and how to avoid them

🎛️ Controlled Components: React Takes Control

Understanding Controlled Components

In controlled components, React state is the "single source of truth" for form inputs. The input's value is always driven by React state, and changes flow through event handlers.

jsx
function ControlledInputBasics() {
  const [inputValue, setInputValue] = useState("");
  const [textareaValue, setTextareaValue] = useState("");
  const [selectValue, setSelectValue] = useState("");
  const [checkboxValue, setCheckboxValue] = useState(false);
  const [radioValue, setRadioValue] = useState("");

  // ✅ Controlled input - value comes from state
  const handleInputChange = (event) => {
    setInputValue(event.target.value);
  };

  const handleTextareaChange = (event) => {
    setTextareaValue(event.target.value);
  };

  const handleSelectChange = (event) => {
    setSelectValue(event.target.value);
  };

  const handleCheckboxChange = (event) => {
    setCheckboxValue(event.target.checked);
  };

  const handleRadioChange = (event) => {
    setRadioValue(event.target.value);
  };

  return (
    <div className="controlled-demo">
      <h3>Controlled Components Demo</h3>

      {/* Text Input */}
      <div className="form-group">
        <label htmlFor="controlled-input">Text Input:</label>
        <input
          type="text"
          id="controlled-input"
          value={inputValue} // ✅ Value controlled by React state
          onChange={handleInputChange} // ✅ Changes update state
          placeholder="Type something..."
        />
        <p>Current value: "{inputValue}"</p>
        <p>Length: {inputValue.length}</p>
      </div>

      {/* Textarea */}
      <div className="form-group">
        <label htmlFor="controlled-textarea">Textarea:</label>
        <textarea
          id="controlled-textarea"
          value={textareaValue}
          onChange={handleTextareaChange}
          placeholder="Enter multiple lines..."
          rows={4}
        />
        <p>Lines: {textareaValue.split("\n").length}</p>
        <p>
          Words:{" "}
          {textareaValue.trim() ? textareaValue.trim().split(/\s+/).length : 0}
        </p>
      </div>

      {/* Select Dropdown */}
      <div className="form-group">
        <label htmlFor="controlled-select">Select:</label>
        <select
          id="controlled-select"
          value={selectValue}
          onChange={handleSelectChange}
        >
          <option value="">Choose an option...</option>
          <option value="option1">Option 1</option>
          <option value="option2">Option 2</option>
          <option value="option3">Option 3</option>
        </select>
        <p>Selected: {selectValue || "None"}</p>
      </div>

      {/* Checkbox */}
      <div className="form-group">
        <label>
          <input
            type="checkbox"
            checked={checkboxValue} // ✅ Use 'checked' for checkboxes
            onChange={handleCheckboxChange}
          />
          Checkbox Option
        </label>
        <p>Checked: {checkboxValue ? "Yes" : "No"}</p>
      </div>

      {/* Radio Buttons */}
      <div className="form-group">
        <fieldset>
          <legend>Radio Options:</legend>
          {["radio1", "radio2", "radio3"].map((option) => (
            <label key={option}>
              <input
                type="radio"
                name="controlled-radio"
                value={option}
                checked={radioValue === option} // ✅ Compare with current state
                onChange={handleRadioChange}
              />
              {option.charAt(0).toUpperCase() + option.slice(1)}
            </label>
          ))}
        </fieldset>
        <p>Selected: {radioValue || "None"}</p>
      </div>

      {/* Form State Summary */}
      <div className="state-summary">
        <h4>Current Form State:</h4>
        <pre>
          {JSON.stringify(
            {
              input: inputValue,
              textarea: textareaValue,
              select: selectValue,
              checkbox: checkboxValue,
              radio: radioValue,
            },
            null,
            2
          )}
        </pre>
      </div>
    </div>
  );
}

Advanced Controlled Component Patterns

jsx
function AdvancedControlledPatterns() {
  const [formData, setFormData] = useState({
    username: "",
    email: "",
    password: "",
    confirmPassword: "",
    bio: "",
    skills: [],
    experience: "",
    newsletter: false,
  });

  const [formState, setFormState] = useState({
    errors: {},
    touched: {},
    isSubmitting: false,
    isDirty: false,
  });

  // ✅ Generic handler for all form fields
  const handleFieldChange = (fieldName) => (event) => {
    const { type, value, checked } = event.target;
    const newValue = type === "checkbox" ? checked : value;

    setFormData((prev) => ({
      ...prev,
      [fieldName]: newValue,
    }));

    setFormState((prev) => ({
      ...prev,
      isDirty: true,
      touched: { ...prev.touched, [fieldName]: true },
    }));

    // Real-time validation
    validateField(fieldName, newValue);
  };

  // ✅ Multi-select handler
  const handleMultiSelectChange = (fieldName) => (event) => {
    const { value, checked } = event.target;

    setFormData((prev) => {
      const currentArray = prev[fieldName] || [];
      const newArray = checked
        ? [...currentArray, value]
        : currentArray.filter((item) => item !== value);

      return {
        ...prev,
        [fieldName]: newArray,
      };
    });
  };

  // ✅ Input transformation (e.g., formatting)
  const handleFormattedChange = (fieldName, formatter) => (event) => {
    const rawValue = event.target.value;
    const formattedValue = formatter(rawValue);

    setFormData((prev) => ({
      ...prev,
      [fieldName]: formattedValue,
    }));
  };

  // Formatters
  const formatters = {
    phone: (value) => {
      // Format as (XXX) XXX-XXXX
      const numbers = value.replace(/\D/g, "");
      if (numbers.length <= 3) return numbers;
      if (numbers.length <= 6)
        return `(${numbers.slice(0, 3)}) ${numbers.slice(3)}`;
      return `(${numbers.slice(0, 3)}) ${numbers.slice(3, 6)}-${numbers.slice(
        6,
        10
      )}`;
    },

    currency: (value) => {
      // Format as currency
      const numbers = value.replace(/[^\d.]/g, "");
      const parts = numbers.split(".");
      if (parts.length > 2) return parts[0] + "." + parts[1];
      return numbers;
    },

    uppercase: (value) => value.toUpperCase(),

    noSpaces: (value) => value.replace(/\s/g, ""),
  };

  // Validation
  const validateField = (fieldName, value) => {
    const errors = { ...formState.errors };

    switch (fieldName) {
      case "username":
        if (!value.trim()) {
          errors.username = "Username is required";
        } else if (value.length < 3) {
          errors.username = "Username must be at least 3 characters";
        } else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
          errors.username =
            "Username can only contain letters, numbers, and underscores";
        } else {
          delete errors.username;
        }
        break;

      case "email":
        if (!value.trim()) {
          errors.email = "Email is required";
        } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
          errors.email = "Please enter a valid email address";
        } else {
          delete errors.email;
        }
        break;

      case "password":
        if (!value) {
          errors.password = "Password is required";
        } else if (value.length < 8) {
          errors.password = "Password must be at least 8 characters";
        } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
          errors.password =
            "Password must contain uppercase, lowercase, and number";
        } else {
          delete errors.password;
        }
        break;

      case "confirmPassword":
        if (value !== formData.password) {
          errors.confirmPassword = "Passwords do not match";
        } else {
          delete errors.confirmPassword;
        }
        break;
    }

    setFormState((prev) => ({
      ...prev,
      errors,
    }));
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    // Validate all fields
    Object.keys(formData).forEach((field) => {
      validateField(field, formData[field]);
    });

    const hasErrors = Object.keys(formState.errors).length > 0;

    if (!hasErrors) {
      setFormState((prev) => ({ ...prev, isSubmitting: true }));

      // Simulate API call
      setTimeout(() => {
        console.log("Form submitted:", formData);
        alert("Form submitted successfully!");
        setFormState((prev) => ({ ...prev, isSubmitting: false }));
      }, 2000);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="advanced-controlled-form">
      <h3>Advanced Controlled Form Patterns</h3>

      {/* Basic text input with validation */}
      <div className="form-group">
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          value={formData.username}
          onChange={handleFieldChange("username")}
          className={formState.errors.username ? "error" : ""}
        />
        {formState.errors.username && (
          <span className="error-message">{formState.errors.username}</span>
        )}
      </div>

      {/* Email with validation */}
      <div className="form-group">
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          value={formData.email}
          onChange={handleFieldChange("email")}
          className={formState.errors.email ? "error" : ""}
        />
        {formState.errors.email && (
          <span className="error-message">{formState.errors.email}</span>
        )}
      </div>

      {/* Password with strength indicator */}
      <div className="form-group">
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          value={formData.password}
          onChange={handleFieldChange("password")}
          className={formState.errors.password ? "error" : ""}
        />
        {formState.errors.password && (
          <span className="error-message">{formState.errors.password}</span>
        )}

        {/* Password strength indicator */}
        {formData.password && (
          <div className="password-strength">
            <div className="strength-bar">
              <div
                className={`strength-fill ${
                  formData.password.length >= 8 &&
                  /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)
                    ? "strong"
                    : formData.password.length >= 6
                    ? "medium"
                    : "weak"
                }`}
                style={{
                  width: `${Math.min(
                    (formData.password.length / 12) * 100,
                    100
                  )}%`,
                }}
              />
            </div>
          </div>
        )}
      </div>

      {/* Confirm password */}
      <div className="form-group">
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          type="password"
          id="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleFieldChange("confirmPassword")}
          className={formState.errors.confirmPassword ? "error" : ""}
        />
        {formState.errors.confirmPassword && (
          <span className="error-message">
            {formState.errors.confirmPassword}
          </span>
        )}
      </div>

      {/* Formatted phone input */}
      <div className="form-group">
        <label htmlFor="phone">Phone:</label>
        <input
          type="tel"
          id="phone"
          value={formData.phone || ""}
          onChange={handleFormattedChange("phone", formatters.phone)}
          placeholder="(555) 123-4567"
        />
      </div>

      {/* Multi-select checkboxes */}
      <div className="form-group">
        <fieldset>
          <legend>Skills:</legend>
          {["JavaScript", "React", "Node.js", "Python", "SQL"].map((skill) => (
            <label key={skill}>
              <input
                type="checkbox"
                value={skill}
                checked={formData.skills.includes(skill)}
                onChange={handleMultiSelectChange("skills")}
              />
              {skill}
            </label>
          ))}
        </fieldset>
        <p>Selected: {formData.skills.join(", ") || "None"}</p>
      </div>

      {/* Character-limited textarea */}
      <div className="form-group">
        <label htmlFor="bio">Bio (max 500 characters):</label>
        <textarea
          id="bio"
          value={formData.bio}
          onChange={(e) => {
            if (e.target.value.length <= 500) {
              handleFieldChange("bio")(e);
            }
          }}
          rows={4}
          placeholder="Tell us about yourself..."
        />
        <div className="character-count">
          {formData.bio.length}/500 characters
        </div>
      </div>

      {/* Newsletter checkbox */}
      <div className="form-group">
        <label>
          <input
            type="checkbox"
            checked={formData.newsletter}
            onChange={handleFieldChange("newsletter")}
          />
          Subscribe to newsletter
        </label>
      </div>

      {/* Submit button */}
      <button
        type="submit"
        disabled={
          formState.isSubmitting || Object.keys(formState.errors).length > 0
        }
        className="submit-button"
      >
        {formState.isSubmitting ? "Submitting..." : "Submit"}
      </button>

      {/* Form state debug */}
      <details className="form-debug">
        <summary>Form State Debug</summary>
        <pre>{JSON.stringify({ formData, formState }, null, 2)}</pre>
      </details>
    </form>
  );
}

🆓 Uncontrolled Components: Let the DOM Handle It

Understanding Uncontrolled Components

Uncontrolled components let the DOM maintain its own state. You access values using refs when needed (like on form submission).

jsx
function UncontrolledInputBasics() {
  // ✅ Refs to access DOM values
  const inputRef = useRef(null);
  const textareaRef = useRef(null);
  const selectRef = useRef(null);
  const checkboxRef = useRef(null);
  const fileRef = useRef(null);

  const [submittedData, setSubmittedData] = useState(null);

  const handleSubmit = (event) => {
    event.preventDefault();

    // ✅ Access values through refs
    const formData = {
      input: inputRef.current.value,
      textarea: textareaRef.current.value,
      select: selectRef.current.value,
      checkbox: checkboxRef.current.checked,
      file: fileRef.current.files[0]?.name || "No file selected",
    };

    setSubmittedData(formData);
    console.log("Uncontrolled form data:", formData);
  };

  const handleReset = () => {
    // ✅ Reset form using DOM methods
    inputRef.current.value = "";
    textareaRef.current.value = "";
    selectRef.current.value = "";
    checkboxRef.current.checked = false;
    fileRef.current.value = "";
    setSubmittedData(null);
  };

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div className="uncontrolled-demo">
      <h3>Uncontrolled Components Demo</h3>

      <form onSubmit={handleSubmit}>
        {/* Text Input with default value */}
        <div className="form-group">
          <label htmlFor="uncontrolled-input">Text Input:</label>
          <input
            type="text"
            id="uncontrolled-input"
            ref={inputRef}
            defaultValue="Default text" // ✅ Use defaultValue, not value
            placeholder="Type something..."
          />
        </div>

        {/* Textarea */}
        <div className="form-group">
          <label htmlFor="uncontrolled-textarea">Textarea:</label>
          <textarea
            id="uncontrolled-textarea"
            ref={textareaRef}
            defaultValue="Default textarea content"
            rows={4}
          />
        </div>

        {/* Select */}
        <div className="form-group">
          <label htmlFor="uncontrolled-select">Select:</label>
          <select
            id="uncontrolled-select"
            ref={selectRef}
            defaultValue="option2" // ✅ Default selection
          >
            <option value="">Choose an option...</option>
            <option value="option1">Option 1</option>
            <option value="option2">Option 2</option>
            <option value="option3">Option 3</option>
          </select>
        </div>

        {/* Checkbox */}
        <div className="form-group">
          <label>
            <input
              type="checkbox"
              ref={checkboxRef}
              defaultChecked={true} // ✅ Use defaultChecked
            />
            Checkbox Option
          </label>
        </div>

        {/* File Input (always uncontrolled) */}
        <div className="form-group">
          <label htmlFor="file-input">File:</label>
          <input
            type="file"
            id="file-input"
            ref={fileRef}
            accept=".txt,.pdf,.doc,.docx"
          />
        </div>

        <div className="form-actions">
          <button type="submit">Submit</button>
          <button type="button" onClick={handleReset}>
            Reset
          </button>
          <button type="button" onClick={focusInput}>
            Focus Input
          </button>
        </div>
      </form>

      {/* Display submitted data */}
      {submittedData && (
        <div className="submitted-data">
          <h4>Submitted Data:</h4>
          <pre>{JSON.stringify(submittedData, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

Advanced Uncontrolled Patterns

jsx
function AdvancedUncontrolledPatterns() {
  const formRef = useRef(null);
  const [validationErrors, setValidationErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // ✅ Validation using FormData API
  const validateForm = (formData) => {
    const errors = {};

    const username = formData.get("username");
    const email = formData.get("email");
    const password = formData.get("password");
    const confirmPassword = formData.get("confirmPassword");
    const age = formData.get("age");

    if (!username || username.length < 3) {
      errors.username = "Username must be at least 3 characters";
    }

    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.email = "Please enter a valid email";
    }

    if (!password || password.length < 8) {
      errors.password = "Password must be at least 8 characters";
    }

    if (password !== confirmPassword) {
      errors.confirmPassword = "Passwords do not match";
    }

    if (!age || parseInt(age) < 13 || parseInt(age) > 120) {
      errors.age = "Please enter a valid age (13-120)";
    }

    return errors;
  };

  const handleSubmit = async (event) => {
    event.preventDefault();

    // ✅ Use FormData API for uncontrolled forms
    const formData = new FormData(formRef.current);

    // Convert FormData to regular object for easier handling
    const data = Object.fromEntries(formData.entries());

    // Handle checkboxes (they won't appear in FormData if unchecked)
    data.newsletter = formData.has("newsletter");

    // Handle multiple checkboxes
    data.skills = formData.getAll("skills");

    console.log("Form data:", data);

    // Validate
    const errors = validateForm(formData);
    setValidationErrors(errors);

    if (Object.keys(errors).length === 0) {
      setIsSubmitting(true);

      try {
        // Simulate API call
        await new Promise((resolve) => setTimeout(resolve, 2000));

        alert("Form submitted successfully!");
        formRef.current.reset(); // ✅ Reset entire form
        setValidationErrors({});
      } catch (error) {
        console.error("Submission error:", error);
      } finally {
        setIsSubmitting(false);
      }
    }
  };

  // ✅ Real-time validation on blur
  const handleFieldBlur = (event) => {
    const { name, value } = event.target;
    const tempFormData = new FormData();
    tempFormData.set(name, value);

    // Get all current form values for cross-field validation
    const currentFormData = new FormData(formRef.current);
    const errors = validateForm(currentFormData);

    setValidationErrors((prev) => ({
      ...prev,
      [name]: errors[name],
    }));
  };

  return (
    <div className="advanced-uncontrolled-demo">
      <h3>Advanced Uncontrolled Form Patterns</h3>

      <form ref={formRef} onSubmit={handleSubmit}>
        {/* Username */}
        <div className="form-group">
          <label htmlFor="username">Username:</label>
          <input
            type="text"
            id="username"
            name="username" // ✅ Name attribute is crucial
            onBlur={handleFieldBlur}
            className={validationErrors.username ? "error" : ""}
          />
          {validationErrors.username && (
            <span className="error-message">{validationErrors.username}</span>
          )}
        </div>

        {/* Email */}
        <div className="form-group">
          <label htmlFor="email">Email:</label>
          <input
            type="email"
            id="email"
            name="email"
            onBlur={handleFieldBlur}
            className={validationErrors.email ? "error" : ""}
          />
          {validationErrors.email && (
            <span className="error-message">{validationErrors.email}</span>
          )}
        </div>

        {/* Password */}
        <div className="form-group">
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            id="password"
            name="password"
            onBlur={handleFieldBlur}
            className={validationErrors.password ? "error" : ""}
          />
          {validationErrors.password && (
            <span className="error-message">{validationErrors.password}</span>
          )}
        </div>

        {/* Confirm Password */}
        <div className="form-group">
          <label htmlFor="confirmPassword">Confirm Password:</label>
          <input
            type="password"
            id="confirmPassword"
            name="confirmPassword"
            onBlur={handleFieldBlur}
            className={validationErrors.confirmPassword ? "error" : ""}
          />
          {validationErrors.confirmPassword && (
            <span className="error-message">
              {validationErrors.confirmPassword}
            </span>
          )}
        </div>

        {/* Age */}
        <div className="form-group">
          <label htmlFor="age">Age:</label>
          <input
            type="number"
            id="age"
            name="age"
            min="13"
            max="120"
            onBlur={handleFieldBlur}
            className={validationErrors.age ? "error" : ""}
          />
          {validationErrors.age && (
            <span className="error-message">{validationErrors.age}</span>
          )}
        </div>

        {/* Country Select */}
        <div className="form-group">
          <label htmlFor="country">Country:</label>
          <select id="country" name="country" defaultValue="">
            <option value="">Select a country</option>
            <option value="us">United States</option>
            <option value="ca">Canada</option>
            <option value="uk">United Kingdom</option>
            <option value="de">Germany</option>
          </select>
        </div>

        {/* Skills (Multiple Checkboxes) */}
        <div className="form-group">
          <fieldset>
            <legend>Skills:</legend>
            {["JavaScript", "React", "Node.js", "Python"].map((skill) => (
              <label key={skill}>
                <input
                  type="checkbox"
                  name="skills" // ✅ Same name for multiple values
                  value={skill}
                />
                {skill}
              </label>
            ))}
          </fieldset>
        </div>

        {/* Newsletter */}
        <div className="form-group">
          <label>
            <input type="checkbox" name="newsletter" value="yes" />
            Subscribe to newsletter
          </label>
        </div>

        {/* Bio */}
        <div className="form-group">
          <label htmlFor="bio">Bio:</label>
          <textarea
            id="bio"
            name="bio"
            rows={4}
            placeholder="Tell us about yourself..."
          />
        </div>

        {/* Hidden field */}
        <input type="hidden" name="formVersion" value="2.0" />

        <button type="submit" disabled={isSubmitting} className="submit-button">
          {isSubmitting ? "Submitting..." : "Submit"}
        </button>
      </form>
    </div>
  );
}

⚖️ Controlled vs Uncontrolled: When to Use Each

Decision Matrix

jsx
// 📊 Decision Guide:

// ✅ Use CONTROLLED when:
// - Real-time validation needed
// - Input formatting required
// - Conditional field visibility
// - Complex form interactions
// - State needs to be shared with other components
// - You need to prevent certain inputs

function ControlledUseCase() {
  const [creditCard, setCreditCard] = useState("");
  const [expiryDate, setExpiryDate] = useState("");
  const [showCVV, setShowCVV] = useState(false);

  // ✅ Real-time formatting
  const handleCreditCardChange = (event) => {
    let value = event.target.value.replace(/\D/g, ""); // Remove non-digits
    value = value.replace(/(\d{4})(?=\d)/g, "$1 "); // Add spaces
    if (value.length <= 19) {
      // Limit length
      setCreditCard(value);
    }
  };

  // ✅ Conditional field visibility
  useEffect(() => {
    setShowCVV(creditCard.length >= 19); // Show CVV when card number is complete
  }, [creditCard]);

  return (
    <div>
      <input
        type="text"
        value={creditCard}
        onChange={handleCreditCardChange}
        placeholder="1234 5678 9012 3456"
      />

      {showCVV && <input type="text" placeholder="CVV" maxLength={3} />}
    </div>
  );
}

// ✅ Use UNCONTROLLED when:
// - Simple forms with basic validation
// - Performance is critical (large forms)
// - Working with third-party form libraries
// - File uploads
// - You don't need real-time updates

function UncontrolledUseCase() {
  const formRef = useRef();

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(formRef.current);

    // ✅ Simple validation on submit
    const email = formData.get("email");
    if (!email.includes("@")) {
      alert("Please enter a valid email");
      return;
    }

    // Process form...
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input type="email" name="email" required />
      <input type="file" name="resume" accept=".pdf,.doc" />
      <button type="submit">Submit</button>
    </form>
  );
}

Performance Comparison

jsx
function PerformanceComparison() {
  const [renderCount, setRenderCount] = useState(0);
  const [controlledValue, setControlledValue] = useState("");
  const uncontrolledRef = useRef();

  // Track renders
  useEffect(() => {
    setRenderCount((prev) => prev + 1);
  });

  return (
    <div className="performance-demo">
      <h3>Performance Comparison</h3>
      <p>Component renders: {renderCount}</p>

      {/* ❌ Controlled - causes re-render on every keystroke */}
      <div className="controlled-section">
        <h4>Controlled Input (Re-renders on every change)</h4>
        <input
          type="text"
          value={controlledValue}
          onChange={(e) => setControlledValue(e.target.value)}
          placeholder="Type here - watch render count"
        />
        <p>Current value: {controlledValue}</p>
      </div>

      {/* ✅ Uncontrolled - no re-renders during typing */}
      <div className="uncontrolled-section">
        <h4>Uncontrolled Input (No re-renders during typing)</h4>
        <input
          ref={uncontrolledRef}
          type="text"
          placeholder="Type here - render count stays same"
        />
        <button
          onClick={() => {
            alert(`Value: ${uncontrolledRef.current.value}`);
          }}
        >
          Get Value
        </button>
      </div>
    </div>
  );
}

🔄 Hybrid Approaches

Mixing Controlled and Uncontrolled

jsx
function HybridFormApproach() {
  // ✅ Controlled for fields that need real-time updates
  const [searchQuery, setSearchQuery] = useState("");
  const [selectedCategory, setSelectedCategory] = useState("");

  // ✅ Uncontrolled for simple fields
  const formRef = useRef();

  // ✅ Filtered results based on controlled inputs
  const filteredResults = useMemo(() => {
    // Simulate filtering logic
    return mockData.filter(
      (item) =>
        item.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
        (selectedCategory === "" || item.category === selectedCategory)
    );
  }, [searchQuery, selectedCategory]);

  const handleSubmit = (event) => {
    event.preventDefault();

    // Get uncontrolled values
    const formData = new FormData(formRef.current);

    // Combine with controlled values
    const submissionData = {
      searchQuery,
      selectedCategory,
      name: formData.get("name"),
      email: formData.get("email"),
      message: formData.get("message"),
    };

    console.log("Hybrid form submission:", submissionData);
  };

  return (
    <div className="hybrid-form">
      <h3>Hybrid Form Approach</h3>

      {/* Controlled section for real-time features */}
      <div className="search-section">
        <h4>Search & Filter (Controlled)</h4>

        <input
          type="text"
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          placeholder="Search..."
        />

        <select
          value={selectedCategory}
          onChange={(e) => setSelectedCategory(e.target.value)}
        >
          <option value="">All Categories</option>
          <option value="tech">Technology</option>
          <option value="design">Design</option>
          <option value="business">Business</option>
        </select>

        <div className="results">
          <p>Found {filteredResults.length} results</p>
          {/* Render filtered results */}
        </div>
      </div>

      {/* Uncontrolled section for simple form */}
      <form ref={formRef} onSubmit={handleSubmit} className="contact-form">
        <h4>Contact Form (Uncontrolled)</h4>

        <input type="text" name="name" placeholder="Your name" required />

        <input type="email" name="email" placeholder="Your email" required />

        <textarea name="message" placeholder="Your message" rows={4} required />

        <button type="submit">Send Message</button>
      </form>
    </div>
  );
}

Converting Between Patterns

jsx
function ConvertibleForm() {
  const [isControlled, setIsControlled] = useState(false);
  const [controlledValue, setControlledValue] = useState("");
  const uncontrolledRef = useRef();

  const toggleMode = () => {
    if (isControlled) {
      // Converting from controlled to uncontrolled
      // Set the DOM value to match current state
      uncontrolledRef.current.value = controlledValue;
    } else {
      // Converting from uncontrolled to controlled
      // Set state to match current DOM value
      setControlledValue(uncontrolledRef.current.value);
    }
    setIsControlled(!isControlled);
  };

  return (
    <div className="convertible-form">
      <h3>Convertible Form Pattern</h3>

      <button onClick={toggleMode}>
        Switch to {isControlled ? "Uncontrolled" : "Controlled"} Mode
      </button>

      <p>
        Current mode:{" "}
        <strong>{isControlled ? "Controlled" : "Uncontrolled"}</strong>
      </p>

      {isControlled ? (
        <input
          type="text"
          value={controlledValue}
          onChange={(e) => setControlledValue(e.target.value)}
          placeholder="Controlled input"
        />
      ) : (
        <input
          ref={uncontrolledRef}
          type="text"
          defaultValue={controlledValue}
          placeholder="Uncontrolled input"
        />
      )}

      <p>Current value: {isControlled ? controlledValue : "Check on submit"}</p>
    </div>
  );
}

⚠️ Common Mistakes & Anti-Patterns

1. Mixing value and defaultValue

jsx
// ❌ WRONG: Don't mix controlled and uncontrolled props
function MixedPropsError() {
  const [value, setValue] = useState("");

  return (
    <input
      value={value} // Controlled
      defaultValue="initial" // ❌ This will be ignored!
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

// ✅ CORRECT: Choose one pattern
function CorrectPatterns() {
  const [value, setValue] = useState("initial"); // Start with initial value

  return (
    <div>
      {/* Controlled */}
      <input value={value} onChange={(e) => setValue(e.target.value)} />

      {/* OR Uncontrolled */}
      <input defaultValue="initial" ref={someRef} />
    </div>
  );
}

2. Forgetting to handle null/undefined values

jsx
// ❌ WRONG: Can cause "uncontrolled to controlled" warning
function NullValueError() {
  const [value, setValue] = useState(null); // ❌ null makes it uncontrolled initially

  return (
    <input
      value={value} // ❌ null -> string causes warning
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

// ✅ CORRECT: Always use string for controlled inputs
function CorrectNullHandling() {
  const [value, setValue] = useState(""); // ✅ Always string

  return (
    <input
      value={value || ""} // ✅ Ensure it's always a string
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

3. Inefficient controlled components

jsx
// ❌ WRONG: Expensive operations in render
function ExpensiveControlled() {
  const [value, setValue] = useState("");

  // ❌ Expensive operation on every keystroke
  const expensiveValidation = (val) => {
    // Simulate expensive operation
    for (let i = 0; i < 1000000; i++) {
      // Heavy computation
    }
    return val.length > 5;
  };

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <p>Valid: {expensiveValidation(value) ? "Yes" : "No"}</p>
    </div>
  );
}

// ✅ CORRECT: Debounce expensive operations
function OptimizedControlled() {
  const [value, setValue] = useState("");
  const [isValid, setIsValid] = useState(false);

  // ✅ Debounced validation
  const debouncedValidation = useCallback(
    debounce((val) => {
      // Expensive operation only after user stops typing
      const valid = expensiveValidation(val);
      setIsValid(valid);
    }, 300),
    []
  );

  useEffect(() => {
    debouncedValidation(value);
  }, [value, debouncedValidation]);

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <p>Valid: {isValid ? "Yes" : "No"}</p>
    </div>
  );
}

🧪 Mini Challenges

Challenge 1: Smart Form Component

Build a form component that:

  • Automatically chooses controlled vs uncontrolled based on props
  • Supports both real-time and submit-time validation
  • Handles file uploads properly
  • Provides a clean API for both patterns
💡 Solution
jsx
function SmartForm({
  fields,
  onSubmit,
  realTimeValidation = false,
  initialValues = {},
}) {
  const [controlledValues, setControlledValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const formRef = useRef();

  const isControlled =
    realTimeValidation || Object.keys(initialValues).length > 0;

  const validateField = (name, value, fieldConfig) => {
    if (!fieldConfig.validation) return null;

    for (const rule of fieldConfig.validation) {
      if (!rule.test(value)) {
        return rule.message;
      }
    }
    return null;
  };

  const handleControlledChange = (name, fieldConfig) => (event) => {
    const value =
      event.target.type === "checkbox"
        ? event.target.checked
        : event.target.value;

    setControlledValues((prev) => ({ ...prev, [name]: value }));

    if (realTimeValidation) {
      const error = validateField(name, value, fieldConfig);
      setErrors((prev) => ({ ...prev, [name]: error }));
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    let formData;
    if (isControlled) {
      formData = controlledValues;
    } else {
      const formDataObj = new FormData(formRef.current);
      formData = Object.fromEntries(formDataObj.entries());
    }

    // Validate all fields
    const newErrors = {};
    fields.forEach((field) => {
      const error = validateField(field.name, formData[field.name], field);
      if (error) newErrors[field.name] = error;
    });

    setErrors(newErrors);

    if (Object.keys(newErrors).length === 0) {
      onSubmit(formData);
    }
  };

  const renderField = (field) => {
    const commonProps = {
      id: field.name,
      name: field.name,
      type: field.type || "text",
      placeholder: field.placeholder,
      required: field.required,
    };

    if (isControlled) {
      return (
        <input
          {...commonProps}
          value={controlledValues[field.name] || ""}
          onChange={handleControlledChange(field.name, field)}
        />
      );
    } else {
      return (
        <input
          {...commonProps}
          defaultValue={initialValues[field.name] || ""}
        />
      );
    }
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <h3>Smart Form ({isControlled ? "Controlled" : "Uncontrolled"})</h3>

      {fields.map((field) => (
        <div key={field.name} className="form-group">
          <label htmlFor={field.name}>{field.label}:</label>
          {renderField(field)}
          {errors[field.name] && (
            <span className="error">{errors[field.name]}</span>
          )}
        </div>
      ))}

      <button type="submit">Submit</button>
    </form>
  );
}

// Usage examples
function SmartFormExamples() {
  const fields = [
    {
      name: "username",
      label: "Username",
      validation: [{ test: (v) => v.length >= 3, message: "Min 3 characters" }],
    },
    {
      name: "email",
      label: "Email",
      type: "email",
      validation: [{ test: (v) => v.includes("@"), message: "Invalid email" }],
    },
  ];

  return (
    <div>
      {/* Uncontrolled form */}
      <SmartForm
        fields={fields}
        onSubmit={(data) => console.log("Uncontrolled:", data)}
      />

      {/* Controlled form with real-time validation */}
      <SmartForm
        fields={fields}
        realTimeValidation={true}
        initialValues={{ username: "john" }}
        onSubmit={(data) => console.log("Controlled:", data)}
      />
    </div>
  );
}

Challenge 2: Form State Manager Hook

Create a custom hook that:

  • Manages both controlled and uncontrolled forms
  • Provides validation utilities
  • Handles form reset and dirty state
  • Supports field-level and form-level validation
💡 Solution
jsx
function useFormState(initialValues = {}, validationRules = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isDirty, setIsDirty] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const validateField = useCallback(
    (name, value) => {
      const rules = validationRules[name];
      if (!rules) return null;

      for (const rule of rules) {
        if (typeof rule === "function") {
          const result = rule(value, values);
          if (result !== true) return result;
        } else if (rule.test && !rule.test(value, values)) {
          return rule.message;
        }
      }
      return null;
    },
    [validationRules, values]
  );

  const validateForm = useCallback(() => {
    const newErrors = {};
    Object.keys(validationRules).forEach((field) => {
      const error = validateField(field, values[field]);
      if (error) newErrors[field] = error;
    });
    return newErrors;
  }, [validateField, values, validationRules]);

  const setValue = useCallback(
    (name, value) => {
      setValues((prev) => ({ ...prev, [name]: value }));
      setIsDirty(true);

      // Clear error when user starts typing
      if (errors[name]) {
        setErrors((prev) => ({ ...prev, [name]: null }));
      }
    },
    [errors]
  );

  const setFieldTouched = useCallback((name, isTouched = true) => {
    setTouched((prev) => ({ ...prev, [name]: isTouched }));
  }, []);

  const setFieldError = useCallback((name, error) => {
    setErrors((prev) => ({ ...prev, [name]: error }));
  }, []);

  const handleChange = useCallback(
    (name) => (event) => {
      const { type, value, checked } = event.target;
      const newValue = type === "checkbox" ? checked : value;
      setValue(name, newValue);
    },
    [setValue]
  );

  const handleBlur = useCallback(
    (name) => (event) => {
      setFieldTouched(name, true);
      const error = validateField(name, event.target.value);
      setFieldError(name, error);
    },
    [validateField, setFieldTouched, setFieldError]
  );

  const handleSubmit = useCallback(
    (onSubmit) => async (event) => {
      event.preventDefault();

      setIsSubmitting(true);

      // Mark all fields as touched
      const allTouched = Object.keys(validationRules).reduce((acc, field) => {
        acc[field] = true;
        return acc;
      }, {});
      setTouched(allTouched);

      // Validate entire form
      const formErrors = validateForm();
      setErrors(formErrors);

      if (Object.keys(formErrors).length === 0) {
        try {
          await onSubmit(values);
        } catch (error) {
          console.error("Form submission error:", error);
        }
      }

      setIsSubmitting(false);
    },
    [values, validateForm, validationRules]
  );

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsDirty(false);
    setIsSubmitting(false);
  }, [initialValues]);

  const getFieldProps = useCallback(
    (name) => ({
      value: values[name] || "",
      onChange: handleChange(name),
      onBlur: handleBlur(name),
      error: touched[name] ? errors[name] : null,
    }),
    [values, handleChange, handleBlur, touched, errors]
  );

  return {
    values,
    errors,
    touched,
    isDirty,
    isSubmitting,
    setValue,
    setFieldTouched,
    setFieldError,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
    validateForm,
    getFieldProps,
    isValid: Object.keys(errors).length === 0,
  };
}

// Usage example
function FormWithCustomHook() {
  const validationRules = {
    username: [
      {
        test: (v) => v.length >= 3,
        message: "Username must be at least 3 characters",
      },
      {
        test: (v) => /^[a-zA-Z0-9_]+$/.test(v),
        message: "Username can only contain letters, numbers, and underscores",
      },
    ],
    email: [
      {
        test: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
        message: "Please enter a valid email",
      },
    ],
    password: [
      {
        test: (v) => v.length >= 8,
        message: "Password must be at least 8 characters",
      },
    ],
    confirmPassword: [
      (value, allValues) => {
        return value === allValues.password || "Passwords do not match";
      },
    ],
  };

  const form = useFormState(
    { username: "", email: "", password: "", confirmPassword: "" },
    validationRules
  );

  const onSubmit = async (data) => {
    console.log("Form submitted:", data);
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    alert("Form submitted successfully!");
    form.reset();
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <h3>Form with Custom Hook</h3>

      <div className="form-group">
        <label>Username:</label>
        <input
          type="text"
          {...form.getFieldProps("username")}
          className={form.getFieldProps("username").error ? "error" : ""}
        />
        {form.getFieldProps("username").error && (
          <span className="error-message">
            {form.getFieldProps("username").error}
          </span>
        )}
      </div>

      <div className="form-group">
        <label>Email:</label>
        <input
          type="email"
          {...form.getFieldProps("email")}
          className={form.getFieldProps("email").error ? "error" : ""}
        />
        {form.getFieldProps("email").error && (
          <span className="error-message">
            {form.getFieldProps("email").error}
          </span>
        )}
      </div>

      <div className="form-group">
        <label>Password:</label>
        <input
          type="password"
          {...form.getFieldProps("password")}
          className={form.getFieldProps("password").error ? "error" : ""}
        />
        {form.getFieldProps("password").error && (
          <span className="error-message">
            {form.getFieldProps("password").error}
          </span>
        )}
      </div>

      <div className="form-group">
        <label>Confirm Password:</label>
        <input
          type="password"
          {...form.getFieldProps("confirmPassword")}
          className={form.getFieldProps("confirmPassword").error ? "error" : ""}
        />
        {form.getFieldProps("confirmPassword").error && (
          <span className="error-message">
            {form.getFieldProps("confirmPassword").error}
          </span>
        )}
      </div>

      <div className="form-actions">
        <button type="submit" disabled={!form.isValid || form.isSubmitting}>
          {form.isSubmitting ? "Submitting..." : "Submit"}
        </button>
        <button type="button" onClick={form.reset}>
          Reset
        </button>
      </div>

      <div className="form-state">
        <p>Dirty: {form.isDirty ? "Yes" : "No"}</p>
        <p>Valid: {form.isValid ? "Yes" : "No"}</p>
        <p>Submitting: {form.isSubmitting ? "Yes" : "No"}</p>
      </div>
    </form>
  );
}

🎯 When and Why: Decision Framework

Quick Decision Tree

🤔 Should I use Controlled or Uncontrolled?

├── Do you need real-time validation?
│   ├── Yes → Controlled ✅
│   └── No → Continue...
│
├── Do you need to format input as user types?
│   ├── Yes → Controlled ✅
│   └── No → Continue...
│
├── Do you need conditional field visibility?
│   ├── Yes → Controlled ✅
│   └── No → Continue...
│
├── Is it a file input?
│   ├── Yes → Uncontrolled ✅ (files are always uncontrolled)
│   └── No → Continue...
│
├── Is performance critical (large forms)?
│   ├── Yes → Uncontrolled ✅
│   └── No → Continue...
│
├── Do you need to share form state with other components?
│   ├── Yes → Controlled ✅
│   └── No → Continue...
│
└── Simple form with submit-only validation?
    └── Yes → Uncontrolled ✅

Performance Guidelines

jsx
// 📊 Performance Considerations:

// ✅ Controlled: Good for
// - Small to medium forms (< 20 fields)
// - Forms with complex interactions
// - Real-time features

// ✅ Uncontrolled: Good for
// - Large forms (> 20 fields)
// - Simple forms
// - Performance-critical applications
// - Third-party form libraries

// 🎯 Hybrid: Best of both worlds
// - Controlled for interactive fields
// - Uncontrolled for simple fields
// - Use refs for occasional access

🎤 Interview Insights

Common Interview Questions

  1. "What's the difference between controlled and uncontrolled components?"

    • Controlled: React state manages the value
    • Uncontrolled: DOM manages the value, accessed via refs
    • Show code examples of both patterns
  2. "When would you choose controlled over uncontrolled?"

    • Real-time validation
    • Input formatting
    • Conditional rendering
    • State sharing between components
  3. "What's the 'uncontrolled to controlled' warning?"

    • Happens when value changes from null/undefined to string
    • Always initialize controlled inputs with empty string
    • Show how to fix the warning
  4. "How do you handle file uploads in React?"

    • File inputs are always uncontrolled
    • Use refs to access FileList
    • Show proper file handling patterns
  5. "What are the performance implications?"

    • Controlled: Re-renders on every change
    • Uncontrolled: No re-renders during typing
    • When to optimize with debouncing

Code Review Red Flags

jsx
// 🚨 Red Flags in Interviews:

// ❌ Mixing controlled and uncontrolled
<input value={value} defaultValue="bad" />

// ❌ Not handling null values
const [value, setValue] = useState(null);
<input value={value} /> // Warning!

// ❌ Expensive operations in render
<input onChange={() => expensiveValidation()} />

// ❌ Not using proper form submission
<button onClick={() => getValue()}> // Should use form onSubmit

// ❌ Forgetting name attributes for uncontrolled
<input ref={ref} /> // Missing name for FormData

Best Practices to Mention

jsx
// ✅ Interview-worthy patterns:

// 1. Proper controlled initialization
const [value, setValue] = useState(""); // Always string

// 2. Debounced validation
const debouncedValidate = useCallback(debounce(validateField, 300), []);

// 3. Proper form submission
const handleSubmit = (event) => {
  event.preventDefault();
  const formData = new FormData(event.target);
};

// 4. Hybrid approach for performance
const useHybridForm = () => {
  // Controlled for interactive fields
  // Uncontrolled for simple fields
};

// 5. Proper error handling
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
  // Return error or null
};

🎯 Key Takeaways

Decision Matrix Summary

FeatureControlledUncontrolled
Real-time validation✅ Excellent❌ Limited
Input formatting✅ Excellent❌ Not possible
Performance⚠️ Re-renders✅ No re-renders
Simplicity⚠️ More code✅ Less code
File uploads❌ Not possible✅ Required
Form libraries⚠️ Some support✅ Full support
Testing✅ Easy to test⚠️ Requires DOM
Accessibility✅ Full control✅ Native behavior

Mental Model

jsx
// 🧠 Think of it this way:

// Controlled = "React is the boss"
// - React state controls everything
// - Every change goes through React
// - More power, more responsibility

// Uncontrolled = "DOM is the boss"
// - DOM handles its own state
// - React checks in when needed
// - Less power, less responsibility

// Hybrid = "Shared responsibility"
// - React controls what matters
// - DOM handles the rest
// - Best of both worlds

Production Tips

  1. Start with uncontrolled for simple forms
  2. Upgrade to controlled when you need real-time features
  3. Use hybrid approach for complex forms
  4. Always validate on submit regardless of pattern
  5. Consider performance for large forms
  6. Test both patterns in your component library

Next up: useEffect Dependencies & Cleanup - Master React's most powerful hook for side effects and lifecycle management.

Previous: Handling DOM Events


💡 Pro tip: In interviews, always explain your choice between controlled and uncontrolled. Show that you understand the trade-offs and can make informed decisions based on requirements.

PreviousHandling DOM Events: User Interactions MasteryNextuseEffect: Dependencies & Cleanup Mastery

Open source, free forever. Built by iammhador.

Contribute on GitHub