Skip to main content

📋 Forms in React

Forms are everywhere in web applications - login screens, registration pages, search bars, settings panels, checkout flows, and more. But handling forms in React is different from traditional HTML forms. React gives you complete control over form data through state, making forms predictable, testable, and powerful. In this lesson, you'll master controlled components, handle complex form scenarios, implement validation, and build type-safe forms that are a joy to use. Let's make forms great again! 🚀

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Understand controlled vs uncontrolled components
  • Build controlled form inputs with useState
  • Handle multiple form inputs efficiently
  • Implement client-side form validation
  • Type form data safely with TypeScript
  • Handle form submission properly
  • Work with different input types (text, checkbox, radio, select)
  • Build reusable form components
  • Display validation errors effectively

Estimated Time: 75-90 minutes

Project: Build a multi-step registration form with validation

📑 In This Lesson

🎛️ Controlled vs Uncontrolled Components

Before we dive into building forms, you need to understand a fundamental concept in React forms: controlled vs uncontrolled components.

📖 Key Definitions

Controlled Component: A form element whose value is controlled by React state. React is the "single source of truth."

Uncontrolled Component: A form element that maintains its own internal state. The DOM is the source of truth.

Traditional HTML Forms (Uncontrolled)

How HTML Forms Normally Work

<!-- Traditional HTML form -->
<form>
    <input type="text" name="username" />
    <button type="submit">Submit</button>
</form>

<script>
    // You only get the value when form submits
    form.addEventListener('submit', (e) => {
        const username = e.target.username.value;
        console.log(username);
    });
</script>

In traditional HTML:

  • The DOM stores the input value
  • You query the DOM when you need the value
  • You can't easily validate or transform as user types

React Controlled Components

The React Way

const ControlledInput: React.FC = () => {
    // React state is the source of truth
    const [username, setUsername] = useState('');
    
    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={username}  // Value comes from state
                onChange={(e) => setUsername(e.target.value)}  // Update state
            />
            <button type="submit">Submit</button>
        </form>
    );
};

In controlled components:

  • React state stores the value
  • Input displays what's in state
  • onChange updates state, triggering re-render
  • You always know the current value

The Data Flow

sequenceDiagram
    participant User
    participant Input
    participant State
    participant React
    
    User->>Input: Types "A"
    Input->>React: onChange event
    React->>State: setUsername("A")
    State->>React: State updated
    React->>Input: Re-render with value="A"
    Input->>User: Display "A"
    
    Note over State,React: State is single source of truth

Comparison Table

🎨 Visual Comparison

Controlled vs Uncontrolled Components ✅ Controlled Component React State useState("value") value={state} value prop onChange Benefits: ✓ Always know current value ✓ Easy real-time validation ✓ Can transform input ✓ Predictable state ✓ React is source of truth VS ⚠️ Uncontrolled Component DOM stores value defaultValue ref={inputRef} query DOM Limitations: ✗ Must query DOM for value ✗ Hard to validate on change ✗ Can't easily transform ✗ Less predictable ✗ DOM is source of truth
Aspect Controlled Uncontrolled
Source of truth React state DOM
Current value access Always available in state Need to query DOM
Validation Easy - validate on every change Harder - usually on submit
Transform input Easy - modify state Difficult
Complexity More code (state + handler) Less code
React way? ✅ Recommended ⚠️ Rarely used

✅ When to Use Each

Use Controlled Components (99% of the time):

  • Forms with validation
  • When you need the current value
  • Dynamic forms that show/hide based on values
  • When transforming user input (formatting, uppercase, etc.)

Use Uncontrolled Components (rarely):

  • File inputs (must be uncontrolled)
  • Integrating with non-React code
  • Very simple forms where you only need value on submit

Quick Example: Both Approaches

Controlled (Recommended)

const ControlledForm: React.FC = () => {
    const [email, setEmail] = useState('');
    
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        console.log('Email:', email);  // Always have the value!
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input
                value={email}
                onChange={(e) => setEmail(e.target.value)}
            />
            <p>Current: {email}</p>  {/* Can display anytime! */}
        </form>
    );
};

Uncontrolled (Rare)

const UncontrolledForm: React.FC = () => {
    const emailRef = useRef<HTMLInputElement>(null);
    
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        console.log('Email:', emailRef.current?.value);  // Query DOM
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input ref={emailRef} />
            {/* Can't easily show current value */}
        </form>
    );
};

💡 The React Philosophy

React prefers controlled components because they fit the React mental model: state drives the UI. When state changes, UI updates. With controlled components, your form data is just another piece of state - predictable, testable, and React-like.

From now on, we'll focus exclusively on controlled components!

📝 Basic Controlled Inputs

Let's master the fundamentals of controlled inputs, starting simple and building up complexity.

Single Text Input

The Simplest Controlled Input

import React, { useState } from 'react';

const SimpleInput: React.FC = () => {
    const [name, setName] = useState('');
    
    return (
        <div>
            <label htmlFor="name">Name:</label>
            <input
                id="name"
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <p>Hello, {name || 'stranger'}!</p>
        </div>
    );
};

Anatomy of a Controlled Input

Breaking It Down

<input
    id="name"                                    // For label association
    type="text"                                  // Input type
    value={name}                                 // Controlled by state
    onChange={(e) => setName(e.target.value)}    // Update state on change
    placeholder="Enter your name"                // Optional placeholder
    disabled={isLoading}                         // Optional disabled state
/>

Key points:

  • value={name} - Input always shows what's in state
  • onChange - Updates state when user types
  • e.target.value - Gets the new input value
  • Without onChange, input would be read-only!

Email Input with Validation

Real-Time Validation

const EmailInput: React.FC = () => {
    const [email, setEmail] = useState('');
    
    // Validate as user types
    const isValidEmail = email.includes('@') && email.includes('.');
    const showError = email.length > 0 && !isValidEmail;
    
    return (
        <div>
            <label htmlFor="email">Email:</label>
            <input
                id="email"
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                style={{
                    borderColor: showError ? 'red' : '#ddd'
                }}
            />
            {showError && (
                <p style={{ color: 'red', fontSize: '0.875rem' }}>
                    Please enter a valid email
                </p>
            )}
            {isValidEmail && (
                <p style={{ color: 'green', fontSize: '0.875rem' }}>
                    ✓ Looks good!
                </p>
            )}
        </div>
    );
};

Password Input with Strength Indicator

Dynamic Feedback

const PasswordInput: React.FC = () => {
    const [password, setPassword] = useState('');
    const [showPassword, setShowPassword] = useState(false);
    
    // Calculate password strength
    const getStrength = (pwd: string): 'weak' | 'medium' | 'strong' => {
        if (pwd.length < 6) return 'weak';
        if (pwd.length < 10) return 'medium';
        return 'strong';
    };
    
    const strength = getStrength(password);
    const strengthColors = {
        weak: '#f44336',
        medium: '#ff9800',
        strong: '#4CAF50'
    };
    
    return (
        <div>
            <label htmlFor="password">Password:</label>
            <div style={{ position: 'relative' }}>
                <input
                    id="password"
                    type={showPassword ? 'text' : 'password'}
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    style={{ width: '100%', paddingRight: '80px' }}
                />
                <button
                    type="button"
                    onClick={() => setShowPassword(!showPassword)}
                    style={{
                        position: 'absolute',
                        right: '8px',
                        top: '50%',
                        transform: 'translateY(-50%)'
                    }}
                >
                    {showPassword ? '🙈' : '👁️'}
                </button>
            </div>
            
            {password && (
                <div style={{ marginTop: '0.5rem' }}>
                    <div
                        style={{
                            height: '4px',
                            background: '#eee',
                            borderRadius: '2px',
                            overflow: 'hidden'
                        }}
                    >
                        <div
                            style={{
                                height: '100%',
                                width: strength === 'weak' ? '33%' : strength === 'medium' ? '66%' : '100%',
                                background: strengthColors[strength],
                                transition: 'all 0.3s'
                            }}
                        />
                    </div>
                    <p style={{
                        color: strengthColors[strength],
                        fontSize: '0.875rem',
                        marginTop: '0.25rem'
                    }}>
                        Strength: {strength}
                    </p>
                </div>
            )}
        </div>
    );
};

💡 Pro Tips for Inputs

  • Always use htmlFor on labels (connects to input id)
  • Set appropriate type attributes (email, password, tel, etc.)
  • Add placeholder text to guide users
  • Use autoComplete to help browsers fill forms
  • Provide immediate feedback (validation, character count, etc.)

🎛️ Handling Multiple Inputs

Real forms have multiple fields. Let's learn efficient patterns for managing them!

Approach 1: Multiple State Variables

Simple but Verbose

const SimpleForm: React.FC = () => {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [email, setEmail] = useState('');
    const [age, setAge] = useState('');
    
    return (
        <form>
            <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
            <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
            <input value={email} onChange={(e) => setEmail(e.target.value)} />
            <input value={age} onChange={(e) => setAge(e.target.value)} />
        </form>
    );
};

✅ Pros: Simple, explicit
❌ Cons: Repetitive, lots of state variables

Approach 2: Single State Object (Recommended)

Clean and Scalable

interface FormData {
    firstName: string;
    lastName: string;
    email: string;
    age: string;
}

const BetterForm: React.FC = () => {
    const [formData, setFormData] = useState<FormData>({
        firstName: '',
        lastName: '',
        email: '',
        age: ''
    });
    
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value  // Dynamic key!
        }));
    };
    
    return (
        <form>
            <input
                name="firstName"
                value={formData.firstName}
                onChange={handleChange}
            />
            <input
                name="lastName"
                value={formData.lastName}
                onChange={handleChange}
            />
            <input
                name="email"
                value={formData.email}
                onChange={handleChange}
            />
            <input
                name="age"
                value={formData.age}
                onChange={handleChange}
            />
        </form>
    );
};

✅ Pros: One handler, scalable, organized
✅ Use this approach for most forms!

The Power of Dynamic Keys

How [name]: value Works

// When you type in the firstName input:
const name = "firstName";
const value = "John";

// This:
setFormData(prev => ({
    ...prev,
    [name]: value
}));

// Becomes:
{
    firstName: "John",  // Updated!
    lastName: "",
    email: "",
    age: ""
}

The [name] syntax uses the variable value as the key!

Complete Example: Contact Form

Production-Ready Multi-Input Form

import React, { useState } from 'react';

interface ContactFormData {
    name: string;
    email: string;
    subject: string;
    message: string;
}

const ContactForm: React.FC = () => {
    const [formData, setFormData] = useState<ContactFormData>({
        name: '',
        email: '',
        subject: '',
        message: ''
    });
    
    const handleChange = (
        e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
    ) => {
        const { name, value } = e.target;

                

🎮 Interactive Demo: Form State in Action

Type in the fields below and watch the state update in real-time:

setFormData(prev => ({ ...prev, [name]: value })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); console.log('Form submitted:', formData); // Send to API, etc. }; return ( <form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}> <div style={{ marginBottom: '1rem' }}> <label htmlFor="name" style={{ display: 'block', marginBottom: '0.5rem' }}> Name * </label> <input id="name" name="name" type="text" value={formData.name} onChange={handleChange} required style={{ width: '100%', padding: '0.5rem' }} /> </div> <div style={{ marginBottom: '1rem' }}> <label htmlFor="email" style={{ display: 'block', marginBottom: '0.5rem' }}> Email * </label> <input id="email" name="email" type="email" value={formData.email} onChange={handleChange} required style={{ width: '100%', padding: '0.5rem' }} /> </div> <div style={{ marginBottom: '1rem' }}> <label htmlFor="subject" style={{ display: 'block', marginBottom: '0.5rem' }}> Subject * </label> <input id="subject" name="subject" type="text" value={formData.subject} onChange={handleChange} required style={{ width: '100%', padding: '0.5rem' }} /> </div> <div style={{ marginBottom: '1rem' }}> <label htmlFor="message" style={{ display: 'block', marginBottom: '0.5rem' }}> Message * </label> <textarea id="message" name="message" value={formData.message} onChange={handleChange} required rows={5} style={{ width: '100%', padding: '0.5rem', resize: 'vertical' }} /> </div> <button type="submit" style={{ width: '100%', padding: '0.75rem', background: '#667eea', color: 'white', border: 'none', borderRadius: '4px', fontSize: '1rem', cursor: 'pointer' }} > Send Message </button> </form> ); }; export default ContactForm;

✅ Best Practice Pattern

For most forms, use this pattern:

  1. Single state object with interface
  2. One generic handleChange function
  3. Use name attribute to identify inputs
  4. Use computed property names [name]

🎨 Different Input Types

Forms aren't just text boxes! Let's handle checkboxes, radio buttons, selects, and more.

Checkbox Inputs

Single Checkbox

const CheckboxExample: React.FC = () => {
    const [agreed, setAgreed] = useState(false);
    
    return (
        <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
            <input
                type="checkbox"
                checked={agreed}  // Use 'checked', not 'value'!
                onChange={(e) => setAgreed(e.target.checked)}  // Use .checked!
            />
            I agree to the terms and conditions
        </label>
    );
};

Key difference: Use checked and e.target.checked, not value!

Multiple Checkboxes

interface Preferences {
    newsletter: boolean;
    updates: boolean;
    offers: boolean;
}

const MultiCheckbox: React.FC = () => {
    const [preferences, setPreferences] = useState<Preferences>({
        newsletter: false,
        updates: false,
        offers: false
    });
    
    const handleCheckboxChange = (name: keyof Preferences) => {
        setPreferences(prev => ({
            ...prev,
            [name]: !prev[name]  // Toggle the boolean
        }));
    };
    
    return (
        <div>
            <h3>Email Preferences</h3>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="checkbox"
                    checked={preferences.newsletter}
                    onChange={() => handleCheckboxChange('newsletter')}
                />
                {' '}Weekly Newsletter
            </label>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="checkbox"
                    checked={preferences.updates}
                    onChange={() => handleCheckboxChange('updates')}
                />
                {' '}Product Updates
            </label>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="checkbox"
                    checked={preferences.offers}
                    onChange={() => handleCheckboxChange('offers')}
                />
                {' '}Special Offers
            </label>
        </div>
    );
};

Radio Button Inputs

Radio Group

type Size = 'small' | 'medium' | 'large';

const RadioExample: React.FC = () => {
    const [size, setSize] = useState<Size>('medium');
    
    return (
        <div>
            <h3>Select Size</h3>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="radio"
                    name="size"  // Same name groups them together
                    value="small"
                    checked={size === 'small'}
                    onChange={(e) => setSize(e.target.value as Size)}
                />
                {' '}Small
            </label>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="radio"
                    name="size"
                    value="medium"
                    checked={size === 'medium'}
                    onChange={(e) => setSize(e.target.value as Size)}
                />
                {' '}Medium
            </label>
            
            <label style={{ display: 'block', marginBottom: '0.5rem' }}>
                <input
                    type="radio"
                    name="size"
                    value="large"
                    checked={size === 'large'}
                    onChange={(e) => setSize(e.target.value as Size)}
                />
                {' '}Large
            </label>
            
            <p>Selected: {size}</p>
        </div>
    );
};

Select Dropdown

Single Select

const SelectExample: React.FC = () => {
    const [country, setCountry] = useState('');
    
    return (
        <div>
            <label htmlFor="country">Country:</label>
            <select
                id="country"
                value={country}
                onChange={(e) => setCountry(e.target.value)}
                style={{ width: '100%', padding: '0.5rem' }}
            >
                <option value="">Select a country...</option>
                <option value="us">United States</option>
                <option value="uk">United Kingdom</option>
                <option value="ca">Canada</option>
                <option value="au">Australia</option>
            </select>
            
            {country && <p>Selected: {country}</p>}
        </div>
    );
};

Multi-Select

const MultiSelectExample: React.FC = () => {
    const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
    
    const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const options = e.target.options;
        const selected: string[] = [];
        
        for (let i = 0; i < options.length; i++) {
            if (options[i].selected) {
                selected.push(options[i].value);
            }
        }
        
        setSelectedSkills(selected);
    };
    
    return (
        <div>
            <label htmlFor="skills">Skills (hold Ctrl/Cmd to select multiple):</label>
            <select
                id="skills"
                multiple
                value={selectedSkills}
                onChange={handleSelectChange}
                style={{ width: '100%', height: '150px' }}
            >
                <option value="javascript">JavaScript</option>
                <option value="typescript">TypeScript</option>
                <option value="react">React</option>
                <option value="node">Node.js</option>
                <option value="python">Python</option>
            </select>
            
            <p>Selected: {selectedSkills.join(', ') || 'None'}</p>
        </div>
    );
};

Complete Form with All Input Types

Registration Form

interface RegistrationData {
    username: string;
    email: string;
    password: string;
    age: string;
    gender: 'male' | 'female' | 'other' | '';
    country: string;
    newsletter: boolean;
    terms: boolean;
}

const RegistrationForm: React.FC = () => {
    const [formData, setFormData] = useState<RegistrationData>({
        username: '',
        email: '',
        password: '',
        age: '',
        gender: '',
        country: '',
        newsletter: false,
        terms: false
    });
    
    // Handle text inputs
    const handleTextChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
        const { name, value } = e.target;
        setFormData(prev => ({ ...prev, [name]: value }));
    };
    
    // Handle checkboxes
    const handleCheckboxChange = (name: keyof RegistrationData) => {
        setFormData(prev => ({ ...prev, [name]: !prev[name] }));
    };
    
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        console.log('Registration data:', formData);
    };
    
    return (
        <form onSubmit={handleSubmit} style={{ maxWidth: '500px' }}>
            {/* Text input */}
            <div style={{ marginBottom: '1rem' }}>
                <label>Username:</label>
                <input
                    name="username"
                    value={formData.username}
                    onChange={handleTextChange}
                />
            </div>
            
            {/* Radio buttons */}
            <div style={{ marginBottom: '1rem' }}>
                <label>Gender:</label>
                <label>
                    <input
                        type="radio"
                        name="gender"
                        value="male"
                        checked={formData.gender === 'male'}
                        onChange={handleTextChange}
                    /> Male
                </label>
                <label>
                    <input
                        type="radio"
                        name="gender"
                        value="female"
                        checked={formData.gender === 'female'}
                        onChange={handleTextChange}
                    /> Female
                </label>
            </div>
            
            {/* Select dropdown */}
            <div style={{ marginBottom: '1rem' }}>
                <label>Country:</label>
                <select name="country" value={formData.country} onChange={handleTextChange}>
                    <option value="">Select...</option>
                    <option value="us">USA</option>
                    <option value="uk">UK</option>
                </select>
            </div>
            
            {/* Checkboxes */}
            <div style={{ marginBottom: '1rem' }}>
                <label>
                    <input
                        type="checkbox"
                        checked={formData.newsletter}
                        onChange={() => handleCheckboxChange('newsletter')}
                    />
                    Subscribe to newsletter
                </label>
            </div>
            
            <button type="submit">Register</button>

                

🎨 Input Types Visual Reference

React Input Types: Value vs Checked value={state} + onChange 📝 text/email/password value={text} 🔢 number value={num} 📄 textarea value={text} 📋 select value={option} e.target. value // Returns the string value checked={state} + onChange ☑️ checkbox checked={bool} 🔘 radio checked={===} ⚠️ Key Difference: • Use checked, NOT value • Get boolean from e.target.checked e.target. checked // Returns true or false </form> ); };

Input Types Quick Reference

Input Type Value Attribute Get Value From Example
text, email, password value e.target.value Text inputs
checkbox checked e.target.checked Boolean toggles
radio checked e.target.value Single choice from group
select value e.target.value Dropdown selection
textarea value e.target.value Multi-line text
number value e.target.value Numeric input

✅ Form Validation

Good forms validate user input and provide helpful feedback. Let's implement robust validation!

Validation Strategies

Strategy When It Runs Best For
On Change Every keystroke Real-time feedback (email format, etc.)
On Blur When field loses focus After user finishes typing
On Submit When form submitted Final validation check
Combination Multiple triggers Best user experience

Basic Validation Pattern

Validate on Blur and Submit

interface FormData {
    email: string;
    password: string;
}

interface FormErrors {
    email?: string;
    password?: string;
}

const ValidatedForm: React.FC = () => {
    const [formData, setFormData] = useState<FormData>({
        email: '',
        password: ''
    });
    
    const [errors, setErrors] = useState<FormErrors>({});
    const [touched, setTouched] = useState<Record<keyof FormData, boolean>>({
        email: false,
        password: false
    });
    
    // Validation function
    const validateField = (name: keyof FormData, value: string): string | undefined => {
        switch (name) {
            case 'email':
                if (!value) return 'Email is required';
                if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format';
                return undefined;
                
            case 'password':
                if (!value) return 'Password is required';
                if (value.length < 8) return 'Password must be at least 8 characters';
                return undefined;
                
            default:
                return undefined;
        }
    };
    
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setFormData(prev => ({ ...prev, [name]: value }));
        
        // Clear error when user starts typing
        if (touched[name as keyof FormData]) {
            const error = validateField(name as keyof FormData, value);
            setErrors(prev => ({ ...prev, [name]: error }));
        }
    };
    
    const handleBlur = (name: keyof FormData) => {
        setTouched(prev => ({ ...prev, [name]: true }));
        const error = validateField(name, formData[name]);
        setErrors(prev => ({ ...prev, [name]: error }));
    };
    
    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        
        // Validate all fields
        const newErrors: FormErrors = {};
        (Object.keys(formData) as Array<keyof FormData>).forEach(field => {
            const error = validateField(field, formData[field]);
            if (error) newErrors[field] = error;
        });
        
        setErrors(newErrors);
        
        // Mark all as touched
        setTouched({ email: true, password: true });
        
        // If no errors, submit
        if (Object.keys(newErrors).length === 0) {
            console.log('Form is valid! Submitting:', formData);
        }
    };
    
    return (
        <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
            <div style={{ marginBottom: '1rem' }}>
                <label htmlFor="email">Email:</label>
                <input
                    id="email"
                    name="email"
                    type="email"
                    value={formData.email}
                    onChange={handleChange}
                    onBlur={() => handleBlur('email')}
                    style={{
                        width: '100%',
                        padding: '0.5rem',
                        borderColor: touched.email && errors.email ? 'red' : '#ddd'
                    }}
                />
                {touched.email && errors.email && (
                    <p style={{ color: 'red', fontSize: '0.875rem', marginTop: '0.25rem' }}>
                        {errors.email}
                    </p>
                )}
            </div>
            
            <div style={{ marginBottom: '1rem' }}>
                <label htmlFor="password">Password:</label>
                <input
                    id="password"
                    name="password"
                    type="password"
                    value={formData.password}
                    onChange={handleChange}
                    onBlur={() => handleBlur('password')}
                    style={{
                        width: '100%',

                

🎮 Interactive Demo: Validation States

See how validation states change as you type:

padding: '0.5rem', borderColor: touched.password && errors.password ? 'red' : '#ddd' }} /> {touched.password && errors.password && ( <p style={{ color: 'red', fontSize: '0.875rem', marginTop: '0.25rem' }}> {errors.password} </p> )} </div> <button type="submit" style={{ width: '100%', padding: '0.75rem', background: '#667eea', color: 'white', border: 'none', borderRadius: '4px' }} > Submit </button> </form> ); };

💡 Validation Best Practices

  • Touched state: Only show errors after user interacts with field
  • Clear errors on change: Remove error as user fixes it
  • Validate on blur: Check when field loses focus
  • Final validation on submit: Catch anything missed
  • Helpful messages: Tell user what's wrong and how to fix it

Common Validation Rules

Reusable Validators

// Create a validators object
const validators = {
    required: (value: string) => 
        value.trim() === '' ? 'This field is required' : undefined,
    
    email: (value: string) => 
        !/\S+@\S+\.\S+/.test(value) ? 'Invalid email address' : undefined,
    
    minLength: (min: number) => (value: string) =>
        value.length < min ? `Must be at least ${min} characters` : undefined,
    
    maxLength: (max: number) => (value: string) =>
        value.length > max ? `Must be no more than ${max} characters` : undefined,
    
    pattern: (regex: RegExp, message: string) => (value: string) =>
        !regex.test(value) ? message : undefined,
    
    match: (otherValue: string, label: string) => (value: string) =>
        value !== otherValue ? `Must match ${label}` : undefined,
    
    number: (value: string) =>
        isNaN(Number(value)) ? 'Must be a number' : undefined,
    
    url: (value: string) =>
        !/^https?:\/\/.+/.test(value) ? 'Must be a valid URL' : undefined,
    
    phone: (value: string) =>
        !/^\+?[\d\s\-()]+$/.test(value) ? 'Invalid phone number' : undefined
};

// Usage:
const validateEmail = (value: string) => {
    return validators.required(value) || validators.email(value);
};

const validatePassword = (value: string) => {
    return validators.required(value) || validators.minLength(8)(value);
};

Composing Validators

Chain Multiple Rules

// Compose validators for complex rules
const composeValidators = (...validators: Array<(value: string) => string | undefined>) => {
    return (value: string): string | undefined => {
        for (const validator of validators) {
            const error = validator(value);
            if (error) return error;
        }
        return undefined;
    };
};

// Usage - check multiple rules
const validateUsername = composeValidators(
    validators.required,
    validators.minLength(3),
    validators.maxLength(20),
    validators.pattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed')
);

const usernameError = validateUsername('ab');  // "Must be at least 3 characters"

🚀 Form Submission

Let's handle form submission properly, including loading states, success messages, and error handling.

Basic Form Submission

Preventing Default and Handling Submit

const BasicSubmit: React.FC = () => {
    const [formData, setFormData] = useState({ email: '', password: '' });
    
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        // CRITICAL: Prevent page reload!
        e.preventDefault();
        
        console.log('Submitting:', formData);
        // Send to API, etc.
    };
    
    return (
        <form onSubmit={handleSubmit}>
            {/* inputs */}
            <button type="submit">Submit</button>
        </form>
    );
};

⚠️ Always call e.preventDefault() to stop the default form submission!

Submit with Loading State

Async Submission with Feedback

interface FormData {
    email: string;
    password: string;
}

const AsyncForm: React.FC = () => {
    const [formData, setFormData] = useState<FormData>({ email: '', password: '' });
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const [success, setSuccess] = useState(false);
    
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        
        // Reset states
        setError(null);
        setSuccess(false);
        setIsLoading(true);
        
        try {
            // Simulate API call
            await new Promise(resolve => setTimeout(resolve, 2000));
            
            // In real app: await api.login(formData);
            
            setSuccess(true);
            console.log('Login successful!');
            
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Something went wrong');
        } finally {
            setIsLoading(false);
        }
    };
    
    // Don't show form after success
    if (success) {
        return (
            <div style={{ textAlign: 'center', padding: '2rem' }}>
                <h2 style={{ color: '#4CAF50' }}>✅ Success!</h2>
                <p>You have been logged in.</p>
            </div>
        );
    }
    
    return (
        <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
            {error && (
                <div style={{
                    padding: '1rem',
                    background: '#ffebee',
                    border: '1px solid #f44336',
                    borderRadius: '4px',
                    marginBottom: '1rem',
                    color: '#c62828'
                }}>
                    ❌ {error}
                </div>
            )}
            
            <div style={{ marginBottom: '1rem' }}>
                <label htmlFor="email">Email:</label>
                <input
                    id="email"
                    type="email"
                    value={formData.email}
                    onChange={(e) => setFormData({ ...formData, email: e.target.value })}
                    disabled={isLoading}
                    required
                    style={{ width: '100%', padding: '0.5rem' }}
                />
            </div>
            
            <div style={{ marginBottom: '1rem' }}>
                <label htmlFor="password">Password:</label>
                <input
                    id="password"
                    type="password"
                    value={formData.password}

                

🎨 Form Submission Flow

Form Submission: Complete Flow 1. User Clicks Submit Button 🖱️ Click event 2. preventDefault e.preventDefault() ⚠️ CRITICAL! 3. Validate Check all fields ✓ Pass / ✗ Fail 4. Set Loading setLoading(true) ⏳ Disable form 5. API Call await api.submit() 🌐 Send data Success 6a. Success Show message ✅ Reset/Redirect Error 6b. Error setError(msg) ❌ Show error Finally setLoading(false) 🔄 Re-enable form 💡 Without e.preventDefault(): • Page reloads (bad!) • Form data is lost onChange={(e) => setFormData({ ...formData, password: e.target.value })} disabled={isLoading} required style={{ width: '100%', padding: '0.5rem' }} /> </div> <button type="submit" disabled={isLoading} style={{ width: '100%', padding: '0.75rem', background: isLoading ? '#ccc' : '#667eea', color: 'white', border: 'none', borderRadius: '4px', cursor: isLoading ? 'not-allowed' : 'pointer' }} > {isLoading ? 'Logging in...' : 'Login'} </button> </form> ); };

Form Reset After Submission

Clear Form on Success

const ResetForm: React.FC = () => {
    const initialState = { name: '', email: '', message: '' };
    const [formData, setFormData] = useState(initialState);
    
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        
        // Submit form...
        await submitToAPI(formData);
        
        // Reset form to initial state
        setFormData(initialState);
        
        // Or use form ref:
        // e.currentTarget.reset();
    };
    
    return <form onSubmit={handleSubmit}>{/* ... */}</form>;
};

✅ Submission Checklist

  • ✅ Call e.preventDefault()
  • ✅ Show loading state during async operations
  • ✅ Disable form while submitting
  • ✅ Display error messages if submission fails
  • ✅ Show success message or redirect on success
  • ✅ Reset form if appropriate
  • ✅ Handle network errors gracefully

🔒 Type-Safe Forms

TypeScript makes forms safer! Let's use types effectively to catch bugs before they happen.

Strongly Typed Form Data

Define Clear Interfaces

// Define your form shape
interface UserProfileForm {
    firstName: string;
    lastName: string;
    age: number;
    email: string;
    bio: string;
    terms: boolean;
}

// TypeScript ensures you handle all fields!
const ProfileForm: React.FC = () => {
    const [formData, setFormData] = useState<UserProfileForm>({
        firstName: '',
        lastName: '',
        age: 0,
        email: '',
        bio: '',
        terms: false
    });
    
    // Type-safe change handler
    const handleChange = (
        field: keyof UserProfileForm,
        value: string | number | boolean
    ) => {
        setFormData(prev => ({
            ...prev,
            [field]: value
        }));
    };
    
    return <form>{/* ... */}</form>;
};

Type-Safe Event Handlers

Proper Event Types

// Different event types for different elements
const TypeSafeHandlers: React.FC = () => {
    // For input elements
    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        console.log(e.target.value);
    };
    
    // For textarea elements
    const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        console.log(e.target.value);
    };
    
    // For select elements
    const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        console.log(e.target.value);
    };
    
    // For form submission
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
    };
    
    // For button clicks
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        console.log('Button clicked');
    };
    
    return <form>{/* ... */}</form>;
};

Generic Form Component

Reusable Type-Safe Form

// Generic type-safe form hook
function useForm<T extends Record<string, any>>(initialValues: T) {
    const [values, setValues] = useState<T>(initialValues);
    const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
    
    const handleChange = (field: keyof T, value: T[keyof T]) => {
        setValues(prev => ({ ...prev, [field]: value }));
        // Clear error for this field
        setErrors(prev => ({ ...prev, [field]: undefined }));
    };
    
    const reset = () => {
        setValues(initialValues);
        setErrors({});
    };
    
    return {
        values,
        errors,
        setErrors,
        handleChange,
        reset
    };
}

// Usage - fully type-safe!
interface LoginForm {
    email: string;
    password: string;
}

const Login: React.FC = () => {
    const { values, handleChange, reset } = useForm<LoginForm>({
        email: '',
        password: ''
    });
    
    // TypeScript knows values.email and values.password exist!
    return (
        <form>
            <input
                value={values.email}
                onChange={(e) => handleChange('email', e.target.value)}
            />
            <input
                value={values.password}
                onChange={(e) => handleChange('password', e.target.value)}
            />
        </form>
    );
};

💡 Type Safety Benefits

  • Autocomplete: IDE suggests valid field names
  • Catch typos: Misspelled field names cause errors
  • Refactor safely: Rename fields with confidence
  • Documentation: Interface shows form structure
  • Fewer bugs: Type errors caught at compile time

🏋️ Hands-on Practice

Time to build some real forms! These exercises will solidify your understanding.

🏋️ Exercise 1: Survey Form

Goal: Build a survey form with various input types.

Requirements:

  • Text input for name
  • Email input with validation
  • Radio buttons for satisfaction (1-5)
  • Checkboxes for multiple interests
  • Textarea for comments
  • Submit button that logs all data
💡 Hint

Use a single state object and separate handlers for text inputs vs checkboxes.

✅ Solution

Try it yourself first! The patterns are all in the lesson above.

🏋️ Exercise 2: Login Form with Validation

Goal: Create a login form with complete validation.

Requirements:

  • Email and password fields
  • Email format validation
  • Password minimum length (8 characters)
  • Show errors only after field is touched
  • Disable submit button if form invalid
  • Loading state during submission
  • Success/error messages
💡 Hint

Track three states: formData, errors, and touched.

🏋️ Exercise 3: Multi-Step Registration

Goal: Build a multi-step form with navigation.

Requirements:

  • Step 1: Personal info (name, email)
  • Step 2: Account (username, password)
  • Step 3: Preferences (newsletter, notifications)
  • Next/Previous buttons
  • Progress indicator
  • Validate current step before allowing next
  • Review all data on final step
💡 Hint
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({ /* all fields */ });

const nextStep = () => {
    if (validateCurrentStep()) {
        setCurrentStep(prev => prev + 1);
    }
};

🚀 Advanced Patterns

Ready for pro-level form techniques? Let's explore advanced patterns.

Dynamic Form Fields

Add/Remove Fields Dynamically

interface PhoneNumber {
    id: string;
    number: string;
}

const DynamicFields: React.FC = () => {
    const [phones, setPhones] = useState<PhoneNumber[]>([
        { id: '1', number: '' }
    ]);
    
    const addPhone = () => {
        setPhones([...phones, { id: Date.now().toString(), number: '' }]);
    };
    
    const removePhone = (id: string) => {
        setPhones(phones.filter(phone => phone.id !== id));
    };
    
    const updatePhone = (id: string, number: string) => {
        setPhones(phones.map(phone =>
            phone.id === id ? { ...phone, number } : phone
        ));
    };
    
    return (
        <div>
            {phones.map((phone, index) => (
                <div key={phone.id} style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
                    <input
                        type="tel"
                        value={phone.number}
                        onChange={(e) => updatePhone(phone.id, e.target.value)}
                        placeholder={`Phone ${index + 1}`}
                    />
                    {phones.length > 1 && (
                        <button type="button" onClick={() => removePhone(phone.id)}>
                            Remove
                        </button>
                    )}
                </div>
            ))}
            <button type="button" onClick={addPhone}>
                + Add Phone
            </button>
        </div>
    );
};

Conditional Fields

Show/Hide Based on Values

const ConditionalForm: React.FC = () => {
    const [accountType, setAccountType] = useState<'personal' | 'business'>('personal');
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        companyName: '',  // Only for business
        taxId: ''         // Only for business
    });
    
    return (
        <form>
            <div>
                <label>Account Type:</label>
                <select
                    value={accountType}
                    onChange={(e) => setAccountType(e.target.value as any)}
                >
                    <option value="personal">Personal</option>
                    <option value="business">Business</option>
                </select>
            </div>
            
            {/* Always shown */}
            <input
                placeholder="Name"
                value={formData.name}
                onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            />
            
            {/* Conditional fields */}
            {accountType === 'business' && (
                <>
                    <input
                        placeholder="Company Name"
                        value={formData.companyName}
                        onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
                    />
                    <input
                        placeholder="Tax ID"
                        value={formData.taxId}
                        onChange={(e) => setFormData({ ...formData, taxId: e.target.value })}
                    />
                </>
            )}
        </form>
    );
};

Debounced Validation

Validate After User Stops Typing

const DebouncedValidation: React.FC = () => {
    const [username, setUsername] = useState('');
    const [isChecking, setIsChecking] = useState(false);
    const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
    
    // Debounce function
    useEffect(() => {
        if (!username) {
            setIsAvailable(null);
            return;
        }
        
        setIsChecking(true);
        
        const timer = setTimeout(async () => {
            // Check if username is available
            const available = await checkUsernameAvailability(username);
            setIsAvailable(available);
            setIsChecking(false);
        }, 500);  // Wait 500ms after user stops typing
        
        return () => clearTimeout(timer);
    }, [username]);
    
    return (
        <div>
            <input
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                placeholder="Choose username"
            />
            {isChecking && <span>Checking...</span>}
            {!isChecking && isAvailable === true && (
                <span style={{ color: 'green' }}>✓ Available</span>
            )}
            {!isChecking && isAvailable === false && (
                <span style={{ color: 'red' }}>✗ Already taken</span>
            )}
        </div>
    );
};

async function checkUsernameAvailability(username: string): Promise<boolean> {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 500));
    return username !== 'admin';  // Example: 'admin' is taken
}

✨ Best Practices

Follow these guidelines to build forms that users love and developers maintain easily.

✅ 1. Always Use Controlled Components

Keep React state as the single source of truth for form data.

// ✅ Good
const [email, setEmail] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />

// ❌ Bad - uncontrolled
<input defaultValue="[email protected]" />

✅ 2. Provide Immediate Feedback

  • Show validation errors as users type or on blur
  • Display character counts for limited fields
  • Show password strength indicators
  • Indicate required fields clearly

✅ 3. Use Proper Input Types

// ✅ Use semantic types
<input type="email" />    // Mobile keyboards show @
<input type="tel" />      // Numeric keyboard on mobile
<input type="number" />   // Number input
<input type="date" />     // Date picker
<input type="url" />      // URL validation

✅ 4. Accessibility Matters

  • Always use <label> with htmlFor
  • Add aria-label where labels aren't visible
  • Use aria-describedby for error messages
  • Ensure keyboard navigation works
  • Test with screen readers

✅ 5. Handle Loading States

const [isLoading, setIsLoading] = useState(false);

<button type="submit" disabled={isLoading}>
    {isLoading ? 'Submitting...' : 'Submit'}
</button>

// Disable all inputs during submission
<input disabled={isLoading} />

✅ 6. Don't Trust Client-Side Validation

Always validate on the server too! Client-side validation is for UX, not security.

Forms Checklist

Category Checklist Item
Functionality ✅ All inputs controlled by state
✅ Form prevents default submission
✅ Validation works correctly
✅ Loading states handled
User Experience ✅ Clear error messages
✅ Errors shown at right time
✅ Success feedback provided
✅ Submit button disabled when invalid
Accessibility ✅ Labels on all inputs
✅ Keyboard navigation works
✅ Focus management correct
✅ ARIA attributes where needed
Type Safety ✅ Form data interface defined
✅ Event handlers properly typed
✅ No any types used

📚 Summary

Congratulations! You've mastered forms in React - one of the most important skills for building real applications.

What You Learned

🎛️ Controlled Components

  • Difference between controlled and uncontrolled components
  • Why React prefers controlled components
  • How data flows in controlled inputs

📝 Building Forms

  • Creating basic controlled inputs
  • Handling multiple inputs efficiently
  • Working with different input types
  • Managing form state with single object

✅ Validation

  • When to validate (onChange, onBlur, onSubmit)
  • Tracking touched fields
  • Displaying validation errors
  • Creating reusable validators

🚀 Submission

  • Preventing default form behavior
  • Handling async submissions
  • Managing loading states
  • Showing success/error feedback

🔒 Type Safety

  • Typing form data with interfaces
  • Proper event handler types
  • Creating type-safe form utilities

🎯 Key Takeaways

  • Control is power - Controlled components give you full control over form data
  • One state object - Use a single object for related form fields
  • Validate smartly - Validate at the right time for best UX
  • Type everything - TypeScript makes forms safer and easier to maintain
  • Feedback matters - Always show users what's happening
  • Accessibility first - Labels, ARIA, and keyboard support are essential

🚀 Next Steps

Now that you've mastered forms, you're ready for more advanced topics:

📖 Coming Up Next

Lesson 3.4: Lists and Keys

  • Rendering dynamic lists
  • Understanding the key prop
  • CRUD operations on lists
  • List performance optimization

💪 Practice Projects

Build these projects to master forms:

  • Job Application Form - Multi-step with file upload
  • Quiz Builder - Dynamic questions with add/remove
  • Booking System - Date picker, time slots, payment info
  • Profile Editor - Update user info with validation
  • Survey Tool - All input types, skip logic, results summary

✨ Pro Tip

The patterns you learned here are the foundation, but real-world apps often use form libraries like React Hook Form or Formik. These libraries handle the plumbing we built manually, but understanding the fundamentals you learned here is crucial for using them effectively!

🎉 Congratulations!

You've completed Lesson 3.3 and mastered forms in React! You can now build complex, validated, accessible forms that provide great user experiences. This is a critical skill for any React developer. Well done! 🚀

← Previous Lesson 3.2: State Management Patterns Home Next → Lesson 3.4: Lists and Keys