Forms in React: From Simple to Complex

Mastering Controlled Components, Validation, and Form Management

Overview

Welcome to our exploration of forms in React! Forms are like the reception desk of your application - they collect information from users and ensure it's valid before processing. Today, we'll master controlled components, form validation, and advanced form handling techniques that will make your forms both powerful and user-friendly.

Controlled vs Uncontrolled Components

In React, form inputs can be controlled (React manages the value) or uncontrolled (DOM manages the value). Think of controlled components as having React as the puppet master, while uncontrolled components are free spirits.

graph TD A[Form Components] --> B[Controlled] A --> C[Uncontrolled] B --> D[React State] B --> E[Value from Props/State] B --> F[onChange Handler] C --> G[DOM manages value] C --> H[useRef to access] C --> I[defaultValue prop] style B fill:#9cf,stroke:#333,stroke-width:2px style C fill:#f9c,stroke:#333,stroke-width:2px

Controlled Components

Basic Controlled Input


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

    const handleChange = (e) => {
        setValue(e.target.value);
    };

    return (
        <div>
            <input
                type="text"
                value={value}
                onChange={handleChange}
                placeholder="Type something..."
            />
            <p>Current value: {value}</p>
        </div>
    );
}
            

Multiple Inputs


function RegistrationForm() {
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
    });

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prevData => ({
            ...prevData,
            [name]: value
        }));
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', formData);
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="username">Username:</label>
                <input
                    type="text"
                    id="username"
                    name="username"
                    value={formData.username}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="email">Email:</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="password">Password:</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label htmlFor="confirmPassword">Confirm Password:</label>
                <input
                    type="password"
                    id="confirmPassword"
                    name="confirmPassword"
                    value={formData.confirmPassword}
                    onChange={handleChange}
                />
            </div>
            
            <button type="submit">Register</button>
        </form>
    );
}
            

Different Input Types

Textarea


function TextareaExample() {
    const [comment, setComment] = useState('');

    return (
        <div>
            <label htmlFor="comment">Comment:</label>
            <textarea
                id="comment"
                value={comment}
                onChange={(e) => setComment(e.target.value)}
                rows={4}
                cols={50}
            />
            <p>Character count: {comment.length}</p>
        </div>
    );
}
            

Select Dropdown


function SelectExample() {
    const [selectedFruit, setSelectedFruit] = useState('');

    return (
        <div>
            <label htmlFor="fruit">Choose a fruit:</label>
            <select
                id="fruit"
                value={selectedFruit}
                onChange={(e) => setSelectedFruit(e.target.value)}
            >
                <option value="">Select a fruit</option>
                <option value="apple">Apple</option>
                <option value="banana">Banana</option>
                <option value="orange">Orange</option>
            </select>
            <p>Selected: {selectedFruit}</p>
        </div>
    );
}
            

Multiple Select


function MultiSelectExample() {
    const [selectedToppings, setSelectedToppings] = useState([]);

    const handleChange = (e) => {
        const options = [...e.target.selectedOptions];
        const values = options.map(option => option.value);
        setSelectedToppings(values);
    };

    return (
        <div>
            <label htmlFor="toppings">Choose pizza toppings:</label>
            <select
                id="toppings"
                multiple
                value={selectedToppings}
                onChange={handleChange}
            >
                <option value="cheese">Cheese</option>
                <option value="pepperoni">Pepperoni</option>
                <option value="mushrooms">Mushrooms</option>
                <option value="olives">Olives</option>
            </select>
            <p>Selected: {selectedToppings.join(', ')}</p>
        </div>
    );
}
            

Checkboxes


function CheckboxExample() {
    const [preferences, setPreferences] = useState({
        newsletter: false,
        notifications: false,
        marketing: false
    });

    const handleCheckboxChange = (e) => {
        const { name, checked } = e.target;
        setPreferences(prev => ({
            ...prev,
            [name]: checked
        }));
    };

    return (
        <div>
            <h3>Email Preferences</h3>
            <label>
                <input
                    type="checkbox"
                    name="newsletter"
                    checked={preferences.newsletter}
                    onChange={handleCheckboxChange}
                />
                Newsletter
            </label>
            
            <label>
                <input
                    type="checkbox"
                    name="notifications"
                    checked={preferences.notifications}
                    onChange={handleCheckboxChange}
                />
                Notifications
            </label>
            
            <label>
                <input
                    type="checkbox"
                    name="marketing"
                    checked={preferences.marketing}
                    onChange={handleCheckboxChange}
                />
                Marketing emails
            </label>
            
            <pre>{JSON.stringify(preferences, null, 2)}</pre>
        </div>
    );
}
            

Radio Buttons


function RadioExample() {
    const [gender, setGender] = useState('');

    return (
        <div>
            <h3>Select Gender</h3>
            <label>
                <input
                    type="radio"
                    value="male"
                    checked={gender === 'male'}
                    onChange={(e) => setGender(e.target.value)}
                />
                Male
            </label>
            
            <label>
                <input
                    type="radio"
                    value="female"
                    checked={gender === 'female'}
                    onChange={(e) => setGender(e.target.value)}
                />
                Female
            </label>
            
            <label>
                <input
                    type="radio"
                    value="other"
                    checked={gender === 'other'}
                    onChange={(e) => setGender(e.target.value)}
                />
                Other
            </label>
            
            <p>Selected: {gender}</p>
        </div>
    );
}
            

File Inputs


function FileUpload() {
    const [selectedFile, setSelectedFile] = useState(null);
    const [preview, setPreview] = useState('');

    const handleFileChange = (e) => {
        const file = e.target.files[0];
        setSelectedFile(file);

        // Create preview for images
        if (file && file.type.startsWith('image/')) {
            const reader = new FileReader();
            reader.onloadend = () => {
                setPreview(reader.result);
            };
            reader.readAsDataURL(file);
        } else {
            setPreview('');
        }
    };

    return (
        <div>
            <input
                type="file"
                onChange={handleFileChange}
                accept="image/*"
            />
            
            {selectedFile && (
                <div>
                    <p>File name: {selectedFile.name}</p>
                    <p>File size: {(selectedFile.size / 1024).toFixed(2)} KB</p>
                    <p>File type: {selectedFile.type}</p>
                </div>
            )}
            
            {preview && (
                <img 
                    src={preview} 
                    alt="Preview" 
                    style={{ maxWidth: '200px' }}
                />
            )}
        </div>
    );
}
            

Form Validation

Basic Validation


function ValidatedForm() {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });
    const [errors, setErrors] = useState({});

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

        // Email validation
        if (!formData.email) {
            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 < 6) {
            newErrors.password = 'Password must be at least 6 characters';
        }

        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        if (validate()) {
            console.log('Form submitted:', formData);
        }
    };

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
        // Clear error when user starts typing
        if (errors[name]) {
            setErrors(prev => ({
                ...prev,
                [name]: ''
            }));
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="email">Email:</label>
                <input
                    type="text"
                    id="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                    className={errors.email ? 'error' : ''}
                />
                {errors.email && <span className="error">{errors.email}</span>}
            </div>
            
            <div>
                <label htmlFor="password">Password:</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                    className={errors.password ? 'error' : ''}
                />
                {errors.password && <span className="error">{errors.password}</span>}
            </div>
            
            <button type="submit">Submit</button>
        </form>
    );
}
            

Real-time Validation


function RealTimeValidationForm() {
    const [username, setUsername] = useState('');
    const [usernameError, setUsernameError] = useState('');
    const [isChecking, setIsChecking] = useState(false);

    // Debounced username check
    useEffect(() => {
        if (!username) {
            setUsernameError('');
            return;
        }

        const timeoutId = setTimeout(async () => {
            setIsChecking(true);
            try {
                // Simulate API call
                await new Promise(resolve => setTimeout(resolve, 500));
                
                if (username === 'admin' || username === 'user') {
                    setUsernameError('Username is already taken');
                } else {
                    setUsernameError('');
                }
            } finally {
                setIsChecking(false);
            }
        }, 500);

        return () => clearTimeout(timeoutId);
    }, [username]);

    return (
        <div>
            <label htmlFor="username">Username:</label>
            <input
                type="text"
                id="username"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
            />
            {isChecking && <span>Checking...</span>}
            {usernameError && <span className="error">{usernameError}</span>}
            {!isChecking && !usernameError && username && 
                <span className="success">Username available!</span>
            }
        </div>
    );
}
            

Uncontrolled Components


function UncontrolledForm() {
    const formRef = useRef();
    const inputRef = useRef();

    const handleSubmit = (e) => {
        e.preventDefault();
        
        // Access form data
        const formData = new FormData(formRef.current);
        const data = Object.fromEntries(formData.entries());
        console.log('Form data:', data);
        
        // Access specific input
        console.log('Input value:', inputRef.current.value);
    };

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

    return (
        <form ref={formRef} onSubmit={handleSubmit}>
            <div>
                <label htmlFor="name">Name:</label>
                <input
                    type="text"
                    id="name"
                    name="name"
                    ref={inputRef}
                    defaultValue="John"
                />
            </div>
            
            <div>
                <label htmlFor="email">Email:</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    defaultValue="john@example.com"
                />
            </div>
            
            <button type="submit">Submit</button>
            <button type="button" onClick={focusInput}>
                Focus Name Input
            </button>
        </form>
    );
}
            

Custom Form Hook


function useForm(initialValues, validate) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);

    const handleChange = (e) => {
        const { name, value, type, checked } = e.target;
        setValues(prev => ({
            ...prev,
            [name]: type === 'checkbox' ? checked : value
        }));
    };

    const handleBlur = (e) => {
        const { name } = e.target;
        setTouched(prev => ({
            ...prev,
            [name]: true
        }));
        
        // Validate on blur
        if (validate) {
            const validationErrors = validate(values);
            setErrors(validationErrors);
        }
    };

    const handleSubmit = (onSubmit) => async (e) => {
        e.preventDefault();
        setIsSubmitting(true);
        
        if (validate) {
            const validationErrors = validate(values);
            setErrors(validationErrors);
            
            if (Object.keys(validationErrors).length === 0) {
                await onSubmit(values);
            }
        } else {
            await onSubmit(values);
        }
        
        setIsSubmitting(false);
    };

    const resetForm = () => {
        setValues(initialValues);
        setErrors({});
        setTouched({});
        setIsSubmitting(false);
    };

    return {
        values,
        errors,
        touched,
        isSubmitting,
        handleChange,
        handleBlur,
        handleSubmit,
        resetForm
    };
}

// Usage of custom hook
function LoginForm() {
    const validate = (values) => {
        const errors = {};
        if (!values.email) {
            errors.email = 'Email is required';
        }
        if (!values.password) {
            errors.password = 'Password is required';
        }
        return errors;
    };

    const {
        values,
        errors,
        touched,
        isSubmitting,
        handleChange,
        handleBlur,
        handleSubmit
    } = useForm({ email: '', password: '' }, validate);

    const onSubmit = async (values) => {
        console.log('Logging in with:', values);
        // Simulate API call
        await new Promise(resolve => setTimeout(resolve, 1000));
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <input
                    type="email"
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    placeholder="Email"
                />
                {touched.email && errors.email && (
                    <span className="error">{errors.email}</span>
                )}
            </div>
            
            <div>
                <input
                    type="password"
                    name="password"
                    value={values.password}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    placeholder="Password"
                />
                {touched.password && errors.password && (
                    <span className="error">{errors.password}</span>
                )}
            </div>
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Logging in...' : 'Login'}
            </button>
        </form>
    );
}
            

Form Libraries

Popular Form Libraries

graph TD A[React Form Libraries] --> B[React Hook Form] A --> C[Formik] A --> D[Final Form] A --> E[React Final Form] B --> F[Performance focused] B --> G[Minimal re-renders] C --> H[Feature rich] C --> I[Large community] D --> J[Framework agnostic] D --> K[Subscription based] style A fill:#f9f,stroke:#333,stroke-width:2px

Advanced Form Patterns

Dynamic Form Fields


function DynamicForm() {
    const [fields, setFields] = useState([{ id: 1, value: '' }]);

    const addField = () => {
        setFields([...fields, { id: Date.now(), value: '' }]);
    };

    const removeField = (id) => {
        setFields(fields.filter(field => field.id !== id));
    };

    const updateField = (id, value) => {
        setFields(fields.map(field => 
            field.id === id ? { ...field, value } : field
        ));
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form values:', fields.map(f => f.value));
    };

    return (
        <form onSubmit={handleSubmit}>
            {fields.map((field, index) => (
                <div key={field.id}>
                    <input
                        type="text"
                        value={field.value}
                        onChange={(e) => updateField(field.id, e.target.value)}
                        placeholder={`Field ${index + 1}`}
                    />
                    {fields.length > 1 && (
                        <button 
                            type="button" 
                            onClick={() => removeField(field.id)}
                        >
                            Remove
                        </button>
                    )}
                </div>
            ))}
            
            <button type="button" onClick={addField}>
                Add Field
            </button>
            <button type="submit">Submit</button>
        </form>
    );
}
            

Multi-Step Form


function MultiStepForm() {
    const [step, setStep] = useState(1);
    const [formData, setFormData] = useState({
        // Step 1
        firstName: '',
        lastName: '',
        // Step 2
        email: '',
        phone: '',
        // Step 3
        address: '',
        city: ''
    });

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
    };

    const nextStep = () => setStep(prev => prev + 1);
    const prevStep = () => setStep(prev => prev - 1);

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Final form data:', formData);
    };

    const renderStep = () => {
        switch(step) {
            case 1:
                return (
                    <div>
                        <h3>Step 1: Personal Info</h3>
                        <input
                            name="firstName"
                            value={formData.firstName}
                            onChange={handleChange}
                            placeholder="First Name"
                        />
                        <input
                            name="lastName"
                            value={formData.lastName}
                            onChange={handleChange}
                            placeholder="Last Name"
                        />
                    </div>
                );
            case 2:
                return (
                    <div>
                        <h3>Step 2: Contact Info</h3>
                        <input
                            name="email"
                            value={formData.email}
                            onChange={handleChange}
                            placeholder="Email"
                        />
                        <input
                            name="phone"
                            value={formData.phone}
                            onChange={handleChange}
                            placeholder="Phone"
                        />
                    </div>
                );
            case 3:
                return (
                    <div>
                        <h3>Step 3: Address</h3>
                        <input
                            name="address"
                            value={formData.address}
                            onChange={handleChange}
                            placeholder="Address"
                        />
                        <input
                            name="city"
                            value={formData.city}
                            onChange={handleChange}
                            placeholder="City"
                        />
                    </div>
                );
            default:
                return null;
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            {renderStep()}
            
            <div>
                {step > 1 && (
                    <button type="button" onClick={prevStep}>
                        Previous
                    </button>
                )}
                
                {step < 3 ? (
                    <button type="button" onClick={nextStep} >
                        Next
                    </button>
                ) : (
                    <button type="submit">Submit</button>
                )}
            </div>
        </form>
    );
}
            

Best Practices

graph TD A[Form Best Practices] --> B[Use Controlled Components] A --> C[Validate User Input] A --> D[Provide Clear Feedback] A --> E[Optimize Performance] A --> F[Handle Edge Cases] B --> G[Single source of truth] C --> H[Client-side validation] C --> I[Server-side validation] D --> J[Error messages] D --> K[Success indicators] E --> L[Debounce input handlers] E --> M[Memoize callbacks] F --> N[Network errors] F --> O[Browser autofill] style A fill:#9cf,stroke:#333,stroke-width:2px

Practice Exercises

Exercise 1: Registration Form

Create a complete registration form with validation:


function RegistrationForm() {
    // Create a form with:
    // - Username (required, min 3 chars)
    // - Email (required, valid format)
    // - Password (required, min 8 chars, special char)
    // - Confirm password (matches password)
    // - Terms acceptance (required)
    // - Real-time validation
    // - Submit handling
}
            

Exercise 2: Dynamic Survey Form

Build a survey form with dynamic questions:


function SurveyForm() {
    // Create a form that:
    // - Loads questions from an API
    // - Supports multiple question types
    // - Conditional questions
    // - Progress indicator
    // - Saves draft responses
}
            

Exercise 3: File Upload Form

Create a file upload form with preview:


function FileUploadForm() {
    // Build a form that:
    // - Accepts multiple files
    // - Shows preview for images
    // - Validates file size/type
    // - Shows upload progress
    // - Handles errors gracefully
}
            

Key Takeaways

What's Next?

Tomorrow, we'll explore Lists and Conditional Rendering in React. You'll learn how to render dynamic lists efficiently and show or hide components based on conditions!

Homework