Form Handling in React

Building intuitive, reactive, and validated user inputs

Introduction to Forms in React

Forms are the primary way users interact with web applications. From simple contact forms to complex multi-step workflows, mastering form handling is essential for creating engaging user experiences.

In traditional HTML, forms manage their own state and submit data directly to a server. React, with its declarative and component-based approach, gives us more control over form behavior, validation, and submission.

flowchart TD A[User Input] --> B[Form Component] B --> C{Controlled or Uncontrolled?} C -->|Controlled| D[React State] C -->|Uncontrolled| E[DOM] D --> F[Validation] E --> F F --> G[Submission] G --> H[API/Server] style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#ffeecc,stroke:#000 style D fill:#d4f0f0,stroke:#000 style E fill:#d4f0f0,stroke:#000 style F fill:#d4f0f0,stroke:#000 style G fill:#d4f0f0,stroke:#000 style H fill:#d4f0f0,stroke:#000

Controlled vs. Uncontrolled Components

There are two fundamental approaches to handling form elements in React: controlled and uncontrolled components. This distinction is central to understanding React form development.

Controlled Components

In a controlled component, React state is the "single source of truth." Form elements like inputs, selects, and textareas are controlled by React through state and event handlers.

import React, { useState } from 'react';

function ControlledForm() {
    const [name, setName] = useState('');
    
    const handleChange = (event) => {
        setName(event.target.value);
    };
    
    const handleSubmit = (event) => {
        event.preventDefault();
        alert(`Hello, ${name}!`);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <label>
                Name:
                <input 
                    type="text" 
                    value={name} 
                    onChange={handleChange} 
                />
            </label>
            <button type="submit">Submit</button>
        </form>
    );
}

Analogy: A controlled component is like a puppet master controlling every movement of the puppet (form element). Nothing happens without the master's command, giving complete control but requiring more direct management.

Uncontrolled Components

In an uncontrolled component, the DOM itself maintains the form state. We access the values using refs when needed, typically during form submission.

import React, { useRef } from 'react';

function UncontrolledForm() {
    const nameInputRef = useRef();
    
    const handleSubmit = (event) => {
        event.preventDefault();
        alert(`Hello, ${nameInputRef.current.value}!`);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <label>
                Name:
                <input 
                    type="text" 
                    defaultValue="" 
                    ref={nameInputRef} 
                />
            </label>
            <button type="submit">Submit</button>
        </form>
    );
}

Analogy: An uncontrolled component is like giving a car to someone and only asking where they went after the trip is complete. You're not managing the journey, only collecting the final result.

Comparison Table

Aspect Controlled Components Uncontrolled Components
Source of Truth React state DOM
Access to Current Values Immediate (in state) On demand (via ref)
Input Validation Easy, immediate More complex, usually at submission
Conditional Rendering Simple (based on state) More complex
Code Complexity More boilerplate Less code
Ideal For Dynamic forms, real-time validation Simple forms, file inputs

Building a Complete Controlled Form

Let's build a more complete form that demonstrates handling multiple input types, validation, and submission.

import React, { useState } from 'react';

function RegistrationForm() {
    // Form state
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        password: '',
        confirmPassword: '',
        role: 'user',
        interests: [],
        agreeToTerms: false
    });
    
    // Validation state
    const [errors, setErrors] = useState({});
    
    // Handle input changes
    const handleChange = (e) => {
        const { name, value, type, checked } = e.target;
        
        if (type === 'checkbox') {
            if (name === 'agreeToTerms') {
                setFormData({
                    ...formData,
                    [name]: checked
                });
            } else {
                // For checkbox groups (like interests)
                const updatedInterests = [...formData.interests];
                if (checked) {
                    updatedInterests.push(value);
                } else {
                    const index = updatedInterests.indexOf(value);
                    if (index > -1) {
                        updatedInterests.splice(index, 1);
                    }
                }
                setFormData({
                    ...formData,
                    interests: updatedInterests
                });
            }
        } else {
            setFormData({
                ...formData,
                [name]: value
            });
        }
        
        // Clear error when field is edited
        if (errors[name]) {
            setErrors({
                ...errors,
                [name]: null
            });
        }
    };
    
    // Validate form
    const validateForm = () => {
        const newErrors = {};
        
        // Username validation
        if (!formData.username.trim()) {
            newErrors.username = 'Username is required';
        } else if (formData.username.length < 3) {
            newErrors.username = 'Username must be at least 3 characters';
        }
        
        // Email validation
        if (!formData.email.trim()) {
            newErrors.email = 'Email is required';
        } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
            newErrors.email = 'Email is invalid';
        }
        
        // Password validation
        if (!formData.password) {
            newErrors.password = 'Password is required';
        } else if (formData.password.length < 8) {
            newErrors.password = 'Password must be at least 8 characters';
        }
        
        // Confirm password validation
        if (formData.password !== formData.confirmPassword) {
            newErrors.confirmPassword = 'Passwords do not match';
        }
        
        // Terms agreement validation
        if (!formData.agreeToTerms) {
            newErrors.agreeToTerms = 'You must agree to the terms';
        }
        
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0; // Form is valid if no errors
    };
    
    // Handle form submission
    const handleSubmit = (e) => {
        e.preventDefault();
        
        if (validateForm()) {
            // Form is valid, process submission
            console.log('Form submitted:', formData);
            // In a real app, you would send this data to an API
            alert('Registration successful!');
        } else {
            console.log('Form has errors');
        }
    };
    
    return (
        <form onSubmit={handleSubmit} className="registration-form">
            <h2>Create an Account</h2>
            
            {/* Username field */}
            <div className="form-group">
                <label htmlFor="username">Username</label>
                <input
                    type="text"
                    id="username"
                    name="username"
                    value={formData.username}
                    onChange={handleChange}
                    className={errors.username ? 'error' : ''}
                />
                {errors.username && <div className="error-message">{errors.username}</div>}
            </div>
            
            {/* Email field */}
            <div className="form-group">
                <label htmlFor="email">Email</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                    className={errors.email ? 'error' : ''}
                />
                {errors.email && <div className="error-message">{errors.email}</div>}
            </div>
            
            {/* Password field */}
            <div className="form-group">
                <label htmlFor="password">Password</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                    className={errors.password ? 'error' : ''}
                />
                {errors.password && <div className="error-message">{errors.password}</div>}
            </div>
            
            {/* Confirm Password field */}
            <div className="form-group">
                <label htmlFor="confirmPassword">Confirm Password</label>
                <input
                    type="password"
                    id="confirmPassword"
                    name="confirmPassword"
                    value={formData.confirmPassword}
                    onChange={handleChange}
                    className={errors.confirmPassword ? 'error' : ''}
                />
                {errors.confirmPassword && <div className="error-message">{errors.confirmPassword}</div>}
            </div>
            
            {/* Role selection */}
            <div className="form-group">
                <label htmlFor="role">Role</label>
                <select
                    id="role"
                    name="role"
                    value={formData.role}
                    onChange={handleChange}
                >
                    <option value="user">User</option>
                    <option value="editor">Editor</option>
                    <option value="admin">Administrator</option>
                </select>
            </div>
            
            {/* Interests checkboxes */}
            <div className="form-group">
                <label>Interests</label>
                <div className="checkbox-group">
                    <label>
                        <input
                            type="checkbox"
                            name="interests"
                            value="technology"
                            checked={formData.interests.includes('technology')}
                            onChange={handleChange}
                        />
                        Technology
                    </label>
                    <label>
                        <input
                            type="checkbox"
                            name="interests"
                            value="science"
                            checked={formData.interests.includes('science')}
                            onChange={handleChange}
                        />
                        Science
                    </label>
                    <label>
                        <input
                            type="checkbox"
                            name="interests"
                            value="arts"
                            checked={formData.interests.includes('arts')}
                            onChange={handleChange}
                        />
                        Arts
                    </label>
                </div>
            </div>
            
            {/* Terms agreement */}
            <div className="form-group">
                <label className="checkbox-label">
                    <input
                        type="checkbox"
                        name="agreeToTerms"
                        checked={formData.agreeToTerms}
                        onChange={handleChange}
                        className={errors.agreeToTerms ? 'error' : ''}
                    />
                    I agree to the Terms and Conditions
                </label>
                {errors.agreeToTerms && <div className="error-message">{errors.agreeToTerms}</div>}
            </div>
            
            <button type="submit" className="submit-button">Register</button>
        </form>
    );
}

Real-world application: This is similar to a user registration form you might see on sites like GitHub, LinkedIn, or any SaaS product. It includes all the essential form handling concepts: multiple input types, validation, error display, and submission handling.

flowchart TD A[User Types] --> B[handleChange Called] B --> C[Update formData State] A -- Submits Form --> D[handleSubmit Called] D --> E[validateForm Called] E -- Valid --> F[Process Form Data] E -- Invalid --> G[Display Errors] G --> A style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#d4f0f0,stroke:#000 style D fill:#d4f0f0,stroke:#000 style E fill:#ffeecc,stroke:#000 style F fill:#d4f0f0,stroke:#000 style G fill:#ffdddd,stroke:#000

Handling Different Input Types

Different form elements require specific handling techniques. Let's explore how to handle various input types in React.

Text Inputs

// Basic text input
<input
    type="text"
    name="firstName"
    value={formData.firstName}
    onChange={handleChange}
/>

Checkboxes

// Single checkbox (boolean value)
<input
    type="checkbox"
    name="subscribe"
    checked={formData.subscribe}
    onChange={handleChange}
/>

// For the handleChange function:
const handleChange = (e) => {
    const { name, checked, type } = e.target;
    if (type === 'checkbox') {
        setFormData({
            ...formData,
            [name]: checked
        });
    } else {
        // Handle other input types
    }
};

Radio Buttons

// Radio button group
<div>
    <label>
        <input
            type="radio"
            name="gender"
            value="male"
            checked={formData.gender === 'male'}
            onChange={handleChange}
        />
        Male
    </label>
    <label>
        <input
            type="radio"
            name="gender"
            value="female"
            checked={formData.gender === 'female'}
            onChange={handleChange}
        />
        Female
    </label>
    <label>
        <input
            type="radio"
            name="gender"
            value="other"
            checked={formData.gender === 'other'}
            onChange={handleChange}
        />
        Other
    </label>
</div>

Select Dropdowns

// Select dropdown
<select
    name="country"
    value={formData.country}
    onChange={handleChange}
>
    <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="au">Australia</option>
</select>

Multi-select

// Multiple select
<select
    name="languages"
    multiple
    value={formData.languages}
    onChange={(e) => {
        const values = Array.from(
            e.target.selectedOptions,
            option => option.value
        );
        setFormData({
            ...formData,
            languages: values
        });
    }}
>
    <option value="javascript">JavaScript</option>
    <option value="python">Python</option>
    <option value="java">Java</option>
    <option value="csharp">C#</option>
</select>

File Uploads

File inputs are typically handled as uncontrolled components because their values are read-only.

// File input example
function FileUploadForm() {
    const fileInputRef = useRef();
    
    const handleSubmit = (e) => {
        e.preventDefault();
        const file = fileInputRef.current.files[0];
        
        if (file) {
            // Create FormData for API submission
            const formData = new FormData();
            formData.append('file', file);
            
            // Submit to API
            console.log('Uploading file:', file.name);
            // In a real app: await fetch('/api/upload', { method: 'POST', body: formData });
        }
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="file" 
                ref={fileInputRef} 
            />
            <button type="submit">Upload</button>
        </form>
    );
}

Range Sliders

// Range slider
<div>
    <label htmlFor="price-range">Price Range: ${formData.priceRange}</label>
    <input
        type="range"
        id="price-range"
        name="priceRange"
        min="10"
        max="1000"
        step="10"
        value={formData.priceRange}
        onChange={handleChange}
    />
</div>

Form Validation Strategies

Validation ensures that user input meets the required criteria before processing. React gives us flexible options for validating forms.

Client-side Validation Timing

flowchart LR A[Validation Timing] --> B[On Change] A --> C[On Blur] A --> D[On Submit] B --> E[Pros: Immediate feedback] B --> F[Cons: Can be distracting] C --> G[Pros: Good UX balance] C --> H[Cons: Slightly delayed feedback] D --> I[Pros: Non-intrusive] D --> J[Cons: Delayed feedback] style A fill:#d4f0f0,stroke:#000 style B fill:#d4f0f0,stroke:#000 style C fill:#d4f0f0,stroke:#000 style D fill:#d4f0f0,stroke:#000

Validation on Blur Example

import React, { useState } from 'react';

function FormWithBlurValidation() {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });
    
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData({
            ...formData,
            [name]: value
        });
    };
    
    const handleBlur = (e) => {
        const { name } = e.target;
        setTouched({
            ...touched,
            [name]: true
        });
        validateField(name, formData[name]);
    };
    
    const validateField = (name, value) => {
        let error = null;
        
        switch (name) {
            case 'email':
                if (!value) {
                    error = 'Email is required';
                } else if (!/\S+@\S+\.\S+/.test(value)) {
                    error = 'Email is invalid';
                }
                break;
                
            case 'password':
                if (!value) {
                    error = 'Password is required';
                } else if (value.length < 8) {
                    error = 'Password must be at least 8 characters';
                }
                break;
                
            default:
                break;
        }
        
        setErrors(prev => ({
            ...prev,
            [name]: error
        }));
        
        return error === null;
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        
        // Mark all fields as touched
        const allTouched = Object.keys(formData).reduce(
            (acc, key) => ({ ...acc, [key]: true }),
            {}
        );
        setTouched(allTouched);
        
        // Validate all fields
        let isValid = true;
        Object.keys(formData).forEach(name => {
            if (!validateField(name, formData[name])) {
                isValid = false;
            }
        });
        
        if (isValid) {
            console.log('Form is valid, submitting:', formData);
            // Process the form data
        }
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="email">Email</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    className={touched.email && errors.email ? 'error' : ''}
                />
                {touched.email && errors.email && (
                    <div className="error-message">{errors.email}</div>
                )}
            </div>
            
            <div>
                <label htmlFor="password">Password</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    className={touched.password && errors.password ? 'error' : ''}
                />
                {touched.password && errors.password && (
                    <div className="error-message">{errors.password}</div>
                )}
            </div>
            
            <button type="submit">Submit</button>
        </form>
    );
}

Real-world application: This blur validation approach is used in many production forms, such as login forms on banking websites or social media platforms. It provides feedback at an appropriate time without overwhelming the user during input.

Custom Validation Hooks

Creating a custom validation hook can make your form validation reusable across components.

// useFormValidation.js
import { useState, useEffect } from 'react';

const useFormValidation = (initialState, validate) => {
    const [values, setValues] = useState(initialState);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);
    
    // Validate when values change and form is submitting
    useEffect(() => {
        if (isSubmitting) {
            const noErrors = Object.keys(errors).length === 0;
            if (noErrors) {
                // Form can be submitted
                console.log('Form is valid, ready to submit', values);
                setIsSubmitting(false);
            } else {
                setIsSubmitting(false);
            }
        }
    }, [errors, isSubmitting, values]);
    
    // Run validation
    const validateValues = () => {
        const validationErrors = validate(values);
        setErrors(validationErrors);
        return Object.keys(validationErrors).length === 0;
    };
    
    // Handle input changes
    const handleChange = (event) => {
        const { name, value, type, checked } = event.target;
        setValues({
            ...values,
            [name]: type === 'checkbox' ? checked : value
        });
    };
    
    // Handle blur events
    const handleBlur = (event) => {
        const { name } = event.target;
        setTouched({
            ...touched,
            [name]: true
        });
        
        // Validate the field on blur
        const validationErrors = validate({
            ...values
        });
        setErrors(validationErrors);
    };
    
    // Handle form submission
    const handleSubmit = (event) => {
        event.preventDefault();
        
        // Mark all fields as touched
        const allTouched = Object.keys(values).reduce(
            (acc, key) => ({ ...acc, [key]: true }),
            {}
        );
        setTouched(allTouched);
        
        // Validate all fields
        const validationErrors = validate(values);
        setErrors(validationErrors);
        
        // Set submitting to true only if no errors
        if (Object.keys(validationErrors).length === 0) {
            setIsSubmitting(true);
        }
    };
    
    return {
        values,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting
    };
};

export default useFormValidation;

Using the custom hook:

import React from 'react';
import useFormValidation from './useFormValidation';

// Define validation rules
const validateLoginForm = (values) => {
    let errors = {};
    
    // Email validation
    if (!values.email) {
        errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
        errors.email = 'Email is invalid';
    }
    
    // Password validation
    if (!values.password) {
        errors.password = 'Password is required';
    } else if (values.password.length < 8) {
        errors.password = 'Password must be at least 8 characters';
    }
    
    return errors;
};

function LoginForm() {
    const initialState = {
        email: '',
        password: ''
    };
    
    const {
        values,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting
    } = useFormValidation(initialState, validateLoginForm);
    
    // Log in the user
    const loginUser = () => {
        console.log('Logging in with:', values);
        // API call would go here
    };
    
    // When validation passes in the hook, it sets isSubmitting to true
    React.useEffect(() => {
        if (isSubmitting) {
            loginUser();
        }
    }, [isSubmitting]);
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="email">Email</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    className={touched.email && errors.email ? 'error' : ''}
                />
                {touched.email && errors.email && (
                    <div className="error-message">{errors.email}</div>
                )}
            </div>
            
            <div>
                <label htmlFor="password">Password</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={values.password}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    className={touched.password && errors.password ? 'error' : ''}
                />
                {touched.password && errors.password && (
                    <div className="error-message">{errors.password}</div>
                )}
            </div>
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Logging in...' : 'Log In'}
            </button>
        </form>
    );
}

Real-world application: Many production-grade React applications use custom hooks for form validation. This approach is similar to how Formik (a popular form library) handles validation under the hood.

Form Libraries

While building forms from scratch gives you complete control, form libraries can significantly reduce boilerplate and provide battle-tested solutions.

Popular React Form Libraries

Formik Example

import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

// Validation schema using Yup
const SignupSchema = Yup.object().shape({
    firstName: Yup.string()
        .min(2, 'Too Short!')
        .max(50, 'Too Long!')
        .required('Required'),
    lastName: Yup.string()
        .min(2, 'Too Short!')
        .max(50, 'Too Long!')
        .required('Required'),
    email: Yup.string()
        .email('Invalid email')
        .required('Required'),
    password: Yup.string()
        .min(8, 'Password must be at least 8 characters')
        .required('Required')
});

function SignupForm() {
    return (
        <div>
            <h1>Sign Up</h1>
            <Formik
                initialValues={{
                    firstName: '',
                    lastName: '',
                    email: '',
                    password: ''
                }}
                validationSchema={SignupSchema}
                onSubmit={(values, { setSubmitting }) => {
                    setTimeout(() => {
                        alert(JSON.stringify(values, null, 2));
                        setSubmitting(false);
                    }, 400);
                }}
            >
                {({ isSubmitting }) => (
                    <Form>
                        <div>
                            <label htmlFor="firstName">First Name</label>
                            <Field name="firstName" type="text" />
                            <ErrorMessage name="firstName" component="div" className="error" />
                        </div>
                        
                        <div>
                            <label htmlFor="lastName">Last Name</label>
                            <Field name="lastName" type="text" />
                            <ErrorMessage name="lastName" component="div" className="error" />
                        </div>
                        
                        <div>
                            <label htmlFor="email">Email</label>
                            <Field name="email" type="email" />
                            <ErrorMessage name="email" component="div" className="error" />
                        </div>
                        
                        <div>
                            <label htmlFor="password">Password</label>
                            <Field name="password" type="password" />
                            <ErrorMessage name="password" component="div" className="error" />
                        </div>
                        
                        <button type="submit" disabled={isSubmitting}>
                            {isSubmitting ? 'Submitting...' : 'Submit'}
                        </button>
                    </Form>
                )}
            </Formik>
        </div>
    );
}

React Hook Form Example

import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

// Define validation schema
const schema = yup.object().shape({
    username: yup.string().required('Username is required'),
    email: yup.string().email('Must be a valid email').required('Email is required'),
    password: yup.string().min(8, 'Password must be at least 8 characters').required('Password is required')
});

function HookFormExample() {
    const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
        resolver: yupResolver(schema)
    });
    
    const onSubmit = data => {
        console.log(data);
        // API call would go here
    };
    
    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label htmlFor="username">Username</label>
                <input id="username" {...register('username')} />
                {errors.username && <p className="error">{errors.username.message}</p>}
            </div>
            
            <div>
                <label htmlFor="email">Email</label>
                <input id="email" type="email" {...register('email')} />
                {errors.email && <p className="error">{errors.email.message}</p>}
            </div>
            
            <div>
                <label htmlFor="password">Password</label>
                <input id="password" type="password" {...register('password')} />
                {errors.password && <p className="error">{errors.password.message}</p>}
            </div>
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Submitting...' : 'Submit'}
            </button>
        </form>
    );
}

Choosing a form library: The choice between hand-rolling your own forms and using a library depends on your project's complexity:

Dynamic Forms

Many real-world forms need to be dynamic, with fields that can be added, removed, or changed based on user input.

Example: Dynamic Field Addition/Removal

import React, { useState } from 'react';

function DynamicForm() {
    const [formValues, setFormValues] = useState({
        firstName: '',
        lastName: '',
        email: '',
        phoneNumbers: [{ number: '' }]
    });
    
    // Handle changes for basic fields
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormValues({
            ...formValues,
            [name]: value
        });
    };
    
    // Handle phone number changes
    const handlePhoneChange = (index, e) => {
        const { value } = e.target;
        const updatedPhoneNumbers = [...formValues.phoneNumbers];
        updatedPhoneNumbers[index] = { number: value };
        
        setFormValues({
            ...formValues,
            phoneNumbers: updatedPhoneNumbers
        });
    };
    
    // Add a new phone number field
    const addPhoneNumber = () => {
        setFormValues({
            ...formValues,
            phoneNumbers: [...formValues.phoneNumbers, { number: '' }]
        });
    };
    
    // Remove a phone number field
    const removePhoneNumber = (index) => {
        const updatedPhoneNumbers = [...formValues.phoneNumbers];
        updatedPhoneNumbers.splice(index, 1);
        
        setFormValues({
            ...formValues,
            phoneNumbers: updatedPhoneNumbers
        });
    };
    
    // Handle form submission
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', formValues);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="firstName">First Name</label>
                <input
                    type="text"
                    id="firstName"
                    name="firstName"
                    value={formValues.firstName}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="lastName">Last Name</label>
                <input
                    type="text"
                    id="lastName"
                    name="lastName"
                    value={formValues.lastName}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="email">Email</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formValues.email}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label>Phone Numbers</label>
                {formValues.phoneNumbers.map((phone, index) => (
                    <div key={index} className="phone-input-group">
                        <input
                            type="tel"
                            value={phone.number}
                            onChange={(e) => handlePhoneChange(index, e)}
                            placeholder="Phone Number"
                        />
                        {formValues.phoneNumbers.length > 1 && (
                            <button 
                                type="button" 
                                onClick={() => removePhoneNumber(index)}
                                className="remove-button"
                            >
                                Remove
                            </button>
                        )}
                    </div>
                ))}
                <button 
                    type="button" 
                    onClick={addPhoneNumber}
                    className="add-button"
                >
                    Add Phone Number
                </button>
            </div>
            
            <button type="submit">Submit</button>
        </form>
    );
}

Real-world application: Dynamic forms are common in many enterprise applications. For example, an e-commerce website might allow users to add multiple shipping addresses, or a project management tool might let users assign multiple team members to a task.

Conditional Form Fields

import React, { useState } from 'react';

function ConditionalForm() {
    const [formData, setFormData] = useState({
        accountType: 'personal',
        firstName: '',
        lastName: '',
        email: '',
        companyName: '',
        taxId: ''
    });
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData({
            ...formData,
            [name]: value
        });
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', formData);
    };
    
    const isBusinessAccount = formData.accountType === 'business';
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>Account Type</label>
                <select
                    name="accountType"
                    value={formData.accountType}
                    onChange={handleChange}
                >
                    <option value="personal">Personal</option>
                    <option value="business">Business</option>
                </select>
            </div>
            
            {/* These fields show for both account types */}
            <div>
                <label htmlFor="firstName">First Name</label>
                <input
                    type="text"
                    id="firstName"
                    name="firstName"
                    value={formData.firstName}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="lastName">Last Name</label>
                <input
                    type="text"
                    id="lastName"
                    name="lastName"
                    value={formData.lastName}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="email">Email</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
            </div>
            
            {/* These fields only show for business accounts */}
            {isBusinessAccount && (
                <div className="business-fields">
                    <div>
                        <label htmlFor="companyName">Company Name</label>
                        <input
                            type="text"
                            id="companyName"
                            name="companyName"
                            value={formData.companyName}
                            onChange={handleChange}
                        />
                    </div>
                    
                    <div>
                        <label htmlFor="taxId">Tax ID</label>
                        <input
                            type="text"
                            id="taxId"
                            name="taxId"
                            value={formData.taxId}
                            onChange={handleChange}
                        />
                    </div>
                </div>
            )}
            
            <button type="submit">Create Account</button>
        </form>
    );
}

Real-world application: Conditional forms are used in many signup flows, like when creating accounts on platforms that serve both individuals and businesses (e.g., payment processors like Stripe or Paypal, or accounting software like QuickBooks).

Form Accessibility Considerations

Creating accessible forms ensures that all users, including those with disabilities, can interact with your application.

Key Accessibility Guidelines

Accessible Form Example

import React, { useState } from 'react';

function AccessibleForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        topic: '',
        message: '',
        subscribe: false
    });
    
    const [errors, setErrors] = useState({});
    
    const handleChange = (e) => {
        const { name, value, type, checked } = e.target;
        setFormData({
            ...formData,
            [name]: type === 'checkbox' ? checked : value
        });
        
        // Clear errors when field is updated
        if (errors[name]) {
            setErrors({
                ...errors,
                [name]: null
            });
        }
    };
    
    const validateForm = () => {
        const newErrors = {};
        
        if (!formData.name.trim()) {
            newErrors.name = 'Name is required';
        }
        
        if (!formData.email.trim()) {
            newErrors.email = 'Email is required';
        } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
            newErrors.email = 'Email is invalid';
        }
        
        if (!formData.topic) {
            newErrors.topic = 'Please select a topic';
        }
        
        if (!formData.message.trim()) {
            newErrors.message = 'Message is required';
        }
        
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        
        if (validateForm()) {
            console.log('Form submitted:', formData);
            // Submit form data
            alert('Message sent successfully!');
        } else {
            // Set focus to the first field with an error
            const firstErrorField = Object.keys(errors)[0];
            if (firstErrorField && document.getElementById(firstErrorField)) {
                document.getElementById(firstErrorField).focus();
            }
        }
    };
    
    return (
        <form onSubmit={handleSubmit} noValidate>
            <h2 id="contactFormHeading">Contact Us</h2>
            <p>Fields marked with * are required</p>
            
            <div className="form-group">
                <label htmlFor="name" id="name-label">
                    Name *
                </label>
                <input
                    type="text"
                    id="name"
                    name="name"
                    value={formData.name}
                    onChange={handleChange}
                    aria-required="true"
                    aria-describedby={errors.name ? "name-error" : null}
                    aria-invalid={errors.name ? "true" : "false"}
                />
                {errors.name && (
                    <div className="error-message" id="name-error" role="alert">
                        {errors.name}
                    </div>
                )}
            </div>
            
            <div className="form-group">
                <label htmlFor="email" id="email-label">
                    Email *
                </label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                    aria-required="true"
                    aria-describedby={errors.email ? "email-error" : null}
                    aria-invalid={errors.email ? "true" : "false"}
                />
                {errors.email && (
                    <div className="error-message" id="email-error" role="alert">
                        {errors.email}
                    </div>
                )}
            </div>
            
            <div className="form-group">
                <label htmlFor="topic" id="topic-label">
                    Topic *
                </label>
                <select
                    id="topic"
                    name="topic"
                    value={formData.topic}
                    onChange={handleChange}
                    aria-required="true"
                    aria-describedby={errors.topic ? "topic-error" : null}
                    aria-invalid={errors.topic ? "true" : "false"}
                >
                    <option value="">Please select a topic</option>
                    <option value="general">General Inquiry</option>
                    <option value="support">Technical Support</option>
                    <option value="billing">Billing Question</option>
                </select>
                {errors.topic && (
                    <div className="error-message" id="topic-error" role="alert">
                        {errors.topic}
                    </div>
                )}
            </div>
            
            <div className="form-group">
                <label htmlFor="message" id="message-label">
                    Message *
                </label>
                <textarea
                    id="message"
                    name="message"
                    value={formData.message}
                    onChange={handleChange}
                    rows="5"
                    aria-required="true"
                    aria-describedby={errors.message ? "message-error" : null}
                    aria-invalid={errors.message ? "true" : "false"}
                />
                {errors.message && (
                    <div className="error-message" id="message-error" role="alert">
                        {errors.message}
                    </div>
                )}
            </div>
            
            <div className="form-group checkbox-group">
                <input
                    type="checkbox"
                    id="subscribe"
                    name="subscribe"
                    checked={formData.subscribe}
                    onChange={handleChange}
                />
                <label htmlFor="subscribe">
                    Subscribe to our newsletter
                </label>
            </div>
            
            <button type="submit">Send Message</button>
        </form>
    );
}

Real-world application: Government websites, educational institutions, and many corporate sites are required to meet accessibility standards (like WCAG) for their forms. Accessible forms also benefit all users by providing a better user experience.

Form Submission and API Integration

Handling form submission often involves sending data to an API and managing loading/success/error states.

Complete Form with API Submission

import React, { useState } from 'react';
import axios from 'axios';

function SubscriptionForm() {
    const [formData, setFormData] = useState({
        fullName: '',
        email: '',
        plan: 'basic'
    });
    
    const [formState, setFormState] = useState({
        isSubmitting: false,
        isSubmitted: false,
        error: null
    });
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData({
            ...formData,
            [name]: value
        });
    };
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        
        setFormState({
            isSubmitting: true,
            isSubmitted: false,
            error: null
        });
        
        try {
            // Submit form data to API
            const response = await axios.post('https://api.example.com/subscribe', formData);
            
            // Handle successful submission
            setFormState({
                isSubmitting: false,
                isSubmitted: true,
                error: null
            });
            
            // Clear form
            setFormData({
                fullName: '',
                email: '',
                plan: 'basic'
            });
            
            console.log('Subscription created:', response.data);
        } catch (error) {
            // Handle error
            setFormState({
                isSubmitting: false,
                isSubmitted: false,
                error: error.response?.data?.message || 'An error occurred. Please try again.'
            });
            
            console.error('Submission error:', error);
        }
    };
    
    // Show success message after submission
    if (formState.isSubmitted) {
        return (
            <div className="success-message">
                <h2>Subscription Successful!</h2>
                <p>Thank you for subscribing to our service.</p>
                <button 
                    onClick={() => setFormState({ isSubmitting: false, isSubmitted: false, error: null })}
                >
                    Subscribe Another
                </button>
            </div>
        );
    }
    
    return (
        <div>
            <h2>Subscribe to Our Service</h2>
            
            {formState.error && (
                <div className="error-banner" role="alert">
                    {formState.error}
                </div>
            )}
            
            <form onSubmit={handleSubmit}>
                <div className="form-group">
                    <label htmlFor="fullName">Full Name</label>
                    <input
                        type="text"
                        id="fullName"
                        name="fullName"
                        value={formData.fullName}
                        onChange={handleChange}
                        required
                    />
                </div>
                
                <div className="form-group">
                    <label htmlFor="email">Email</label>
                    <input
                        type="email"
                        id="email"
                        name="email"
                        value={formData.email}
                        onChange={handleChange}
                        required
                    />
                </div>
                
                <div className="form-group">
                    <label htmlFor="plan">Subscription Plan</label>
                    <select
                        id="plan"
                        name="plan"
                        value={formData.plan}
                        onChange={handleChange}
                    >
                        <option value="basic">Basic - $9.99/month</option>
                        <option value="pro">Pro - $19.99/month</option>
                        <option value="enterprise">Enterprise - $49.99/month</option>
                    </select>
                </div>
                
                <button 
                    type="submit" 
                    disabled={formState.isSubmitting}
                    className={formState.isSubmitting ? 'submitting' : ''}
                >
                    {formState.isSubmitting ? 'Subscribing...' : 'Subscribe Now'}
                </button>
            </form>
        </div>
    );
}

Real-world application: This pattern is used in nearly every subscription-based service, from streaming platforms to SaaS products, where user information needs to be collected and processed through an API.

Practice Activities

Activity 1: Login Form with Validation

Create a login form with email and password fields

Create a login form with email and password fields. Add client-side validation to ensure:

  • Email field contains a valid email format
  • Password field is at least 8 characters long
  • Show appropriate error messages
  • Disable the submit button when the form is invalid

Add appropriate loading states during form submission and success/error messages after submission.

Activity 2: Multi-step Form

Create a multi-step form with the following steps:

  1. Personal Information (name, email, phone)
  2. Address Information (street, city, state, zip)
  3. Account Setup (username, password, confirm password)
  4. Review and Submit

Include navigation buttons to move between steps, validate each step before proceeding, and display a summary of all information before final submission.

Activity 3: Dynamic Form Builder

Create a simple form builder that allows users to:

  • Add different types of form fields (text, email, select, checkbox)
  • Remove fields
  • Reorder fields by drag and drop or up/down buttons
  • Preview the form they're building

Bonus: Allow users to save and load form templates.

Activity 4: Form with File Upload

Create a profile update form that includes:

  • Basic text fields for profile information
  • A file input for profile picture upload
  • Image preview after selection
  • Validation for file type (only images) and size (max 2MB)

Mock the API submission and handle the form data appropriately.

Summary

Form handling is a fundamental skill for React developers. By mastering the concepts covered in this lecture, you'll be able to create intuitive, accessible, and functional forms that provide an excellent user experience.

Further Resources