Skip to main content

Controlled Form Components: Part 2 Guide

Controlled form components manage all input values through React state. This guide teaches you to build real-world forms handling multiple input types (text, textarea, select, checkbox), implement validation, and provide user feedback. You'll create a reusable handleChange function, validate on submission, and disable submit buttons until the form is valid—essential patterns for production React applications.

Key Takeaways

  • A single generic handleChange function can handle all input types by checking event.target.type and using [name] computed property syntax
  • Checkboxes use checked property; all other inputs use value property
  • Store validation errors in state and render them conditionally next to the relevant fields
  • Call validate() on form submission; only submit if no errors exist
  • Disable the submit button with disabled={!isFormValid()} to improve user experience
  • Use event.preventDefault() in handleSubmit to prevent browser page reload

Prerequisites

Before starting, ensure you understand:

  • Controlled components fundamentals (Part 1)
  • JavaScript ES6 features: arrow functions, destructuring, computed property names
  • React state management with useState
  • Form event handling (onChange, onSubmit)

Building a User Profile Form

Let's build a practical form that collects user information with multiple input types and validation. This form will include:

  • Username (text input) — required, non-empty
  • Bio (textarea) — required, minimum 10 characters
  • Role (select dropdown) — Developer, Designer, or Admin
  • Newsletter (checkbox) — optional subscription

This real-world example covers all common form patterns.

Step 1: Initialize State for Form Data and Errors

Start by creating state objects to store form data and validation errors:

import React, { useState } from 'react';

function UserProfileForm() {
const [profile, setProfile] = useState({
username: '',
bio: '',
role: 'Developer', // Default value for select
newsletter: true, // Default for checkbox
});

const [errors, setErrors] = useState({});

const handleChange = (event) => {
// Implementation next
};

const handleSubmit = (event) => {
// Implementation next
};

return (
<form onSubmit={handleSubmit}>
{/* Form fields here */}
</form>
);
}

export default UserProfileForm;

Explanation:

  • profile object holds all form field values
  • errors object holds validation error messages keyed by field name (e.g., errors.username)
  • Default values for role and newsletter improve UX

Step 2: Create the Generic handleChange Function

A single handleChange function can manage all input types by detecting the input type and using the correct value source:

const handleChange = (event) => {
const { name, value, type, checked } = event.target;

// Checkboxes use 'checked'; all others use 'value'
const newValue = type === 'checkbox' ? checked : value;

setProfile(prevProfile => ({
...prevProfile,
[name]: newValue, // Computed property name
}));
};

Breakdown:

  • Destructure name, value, type, and checked from the event target
  • The ternary operator selects the correct property: checked for checkboxes, value for everything else
  • The spread operator ...prevProfile copies all existing properties
  • [name] is a computed property name—uses the input's name attribute as the object key
  • This pattern works for text inputs, textareas, selects, and checkboxes without modification

Step 3: Render All Form Input Types

Each input type has slightly different JSX, but they all use the same handleChange handler:

return (
<form onSubmit={handleSubmit}>
{/* Text Input */}
<div>
<label>
Username:
<input
type="text"
name="username"
value={profile.username}
onChange={handleChange}
required
/>
</label>
{errors.username && <p style={{ color: 'red' }}>{errors.username}</p>}
</div>

{/* Textarea */}
<div>
<label>
Bio:
<textarea
name="bio"
value={profile.bio}
onChange={handleChange}
rows={4}
/>
</label>
{errors.bio && <p style={{ color: 'red' }}>{errors.bio}</p>}
</div>

{/* Select Dropdown */}
<div>
<label>
Role:
<select name="role" value={profile.role} onChange={handleChange}>
<option value="Developer">Developer</option>
<option value="Designer">Designer</option>
<option value="Admin">Admin</option>
</select>
</label>
</div>

{/* Checkbox */}
<div>
<label>
<input
type="checkbox"
name="newsletter"
checked={profile.newsletter}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
</div>

<button type="submit" disabled={!isFormValid()}>
Save Profile
</button>
</form>
);

Key Points:

  • Each input's name attribute must match a property in the profile state object
  • Text inputs and textareas use value={profile.fieldName}
  • Select dropdowns use value={profile.fieldName} and wrap options
  • Checkboxes use checked={profile.fieldName} instead of value
  • All use the same onChange={handleChange} handler
  • Error messages render conditionally: {errors.fieldName && <p>...}</p>

Step 4: Implement Validation Logic

Create a validate() function that checks form data and returns an object with any errors:

const validate = () => {
const newErrors = {};

if (!profile.username.trim()) {
newErrors.username = 'Username is required';
}

if (profile.bio.length < 10) {
newErrors.bio = 'Bio must be at least 10 characters';
}

// Add more validations as needed
return newErrors;
};

Validation Rules Applied:

  • Username is required and cannot be empty (using trim() to ignore whitespace)
  • Bio must be at least 10 characters long
  • Add more rules by extending this function

Step 5: Handle Form Submission

Implement handleSubmit to prevent default browser behavior, validate, and either show errors or process the form:

const handleSubmit = (event) => {
event.preventDefault(); // Prevent page reload

const newErrors = validate();

if (Object.keys(newErrors).length > 0) {
// Form has errors
setErrors(newErrors);
} else {
// Form is valid
setErrors({});
console.log('Profile submitted:', profile);
alert('Profile saved successfully!');
// In a real app, send data to a server here
}
};

const isFormValid = () => {
const newErrors = validate();
return Object.keys(newErrors).length === 0;
};

Breakdown:

  • event.preventDefault() stops the browser from submitting the form to a server
  • validate() returns an errors object
  • Object.keys(newErrors).length checks if there are any errors
  • If errors exist, update the errors state so they display in the UI
  • If no errors, clear previous errors and submit the form (log to console in this example)

Improving User Experience: Disable Invalid Submit

Disable the submit button while the form is invalid to guide users:

<button type="submit" disabled={!isFormValid()}>
Save Profile
</button>

The button is disabled until isFormValid() returns true. This happens after the user fills required fields and meets all validation requirements. This pattern is common in production apps.

Best Practices for React Forms

Do:

  • Store all form state in a single object or separate pieces of state for clarity
  • Use consistent name attributes that match state keys
  • Validate on submission (or optionally on blur after first attempt)
  • Provide clear, field-specific error messages
  • Use event.preventDefault() in handleSubmit
  • Disable submit buttons for invalid forms when appropriate
  • Use textareas for longer text, not regular inputs
  • Provide default values for select dropdowns

Don't:

  • Don't validate on every keystroke unless absolutely necessary (performance impact)
  • Don't clear all errors on the first keystroke after error display
  • Don't mix controlled and uncontrolled inputs in one form
  • Don't forget the name attribute on form inputs

Frequently Asked Questions

How do I validate only when the user leaves a field (on blur)?

Add an onBlur handler that validates just that field:

const handleBlur = (event) => {
const { name } = event.target;
const newErrors = { ...errors };

if (name === 'username' && !profile.username.trim()) {
newErrors.username = 'Username is required';
}
setErrors(newErrors);
};

<input onBlur={handleBlur} ... />

Can I disable specific buttons without disabling submit?

Yes, use conditional rendering or separate state. Only the submit button should be disabled based on form validity; other buttons (Cancel, Reset) stay enabled.

How do I handle form fields that aren't strings (numbers, dates)?

Store them as strings in state and convert on submission:

const age = parseInt(profile.age, 10);
const date = new Date(profile.date);

Should I validate in handleChange or handleSubmit?

Validate on handleSubmit (submission validation) for best UX. Only validate on handleChange if showing real-time feedback is critical (rare). The combination—validate on submit, then on blur after first attempt—is ideal.

How do I reset the form to initial state?

const handleReset = () => {
setProfile({ username: '', bio: '', role: 'Developer', newsletter: true });
setErrors({});
};

<button type="button" onClick={handleReset}>Reset</button>

Conclusion

Controlled form components are the React standard for handling user input. Master these patterns:

  • Use a single handleChange function with computed property names [name]
  • Store both form data and errors in state
  • Validate on submission before processing data
  • Provide immediate, field-specific error feedback
  • Disable submit buttons for invalid forms to guide users

These patterns form the foundation of professional React form handling. Practice building increasingly complex forms to internalize them.

Further Reading