DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 29: Forms and Form Validation

Forms and Form Validation

Forms are a crucial part of most web applications, allowing users to input data and interact with your application. React provides powerful tools for handling forms, managing form state, and implementing validation. This chapter will cover everything from basic form handling to advanced forms and form validation techniques.

Introduction to Forms in React

React forms can be handled in two main ways:

  • Controlled Components: Form data is handled by React state
  • Uncontrolled Components: Form data is handled by the DOM itself

Controlled vs Uncontrolled Components

Controlled Components are recommended as they give you full control over form data and enable real-time validation.

// Controlled Component Example
import React, { useState } from 'react';

function ControlledForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form Data:', { name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

Uncontrolled Components use refs to access form data:

// Uncontrolled Component Example
import React, { useRef } from 'react';

function UncontrolledForm() {
  const nameRef = useRef();
  const emailRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form Data:', {
      name: nameRef.current.value,
      email: emailRef.current.value
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input type="text" id="name" ref={nameRef} />
      </div>
      <div>
        <label htmlFor="email">Email:</label>
        <input type="email" id="email" ref={emailRef} />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

Basic Form Handling

Single Input Handling

import React, { useState } from 'react';

function BasicForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    age: '',
    gender: '',
    interests: [],
    bio: '',
    newsletter: false
  });

  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
   
    if (type === 'checkbox') {
      if (name === 'interests') {
        // Handle multiple checkboxes
        setFormData(prev => ({
          ...prev,
          interests: checked
            ? [...prev.interests, value]
            : prev.interests.filter(interest => interest !== value)
        }));
      } else {
        // Handle single checkbox
        setFormData(prev => ({
          ...prev,
          [name]: checked
        }));
      }
    } else {
      setFormData(prev => ({
        ...prev,
        [name]: value
      }));
    }
  };

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

  return (
    <form onSubmit={handleSubmit}>
      {/* Text Inputs */}
      <div>
        <label htmlFor="firstName">First Name:</label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          value={formData.firstName}
          onChange={handleInputChange}
          required
        />
      </div>

      <div>
        <label htmlFor="lastName">Last Name:</label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          value={formData.lastName}
          onChange={handleInputChange}
          required
        />
      </div>

      {/* Email Input */}
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          required
        />
      </div>

      {/* Number Input */}
      <div>
        <label htmlFor="age">Age:</label>
        <input
          type="number"
          id="age"
          name="age"
          value={formData.age}
          onChange={handleInputChange}
          min="1"
          max="120"
        />
      </div>

      {/* Select Dropdown */}
      <div>
        <label htmlFor="gender">Gender:</label>
        <select
          id="gender"
          name="gender"
          value={formData.gender}
          onChange={handleInputChange}
        >
          <option value="">Select Gender</option>
          <option value="male">Male</option>
          <option value="female">Female</option>
          <option value="other">Other</option>
        </select>
      </div>

      {/* Multiple Checkboxes */}
      <div>
        <label>Interests:</label>
        {['Reading', 'Sports', 'Music', 'Travel'].map(interest => (
          <label key={interest}>
            <input
              type="checkbox"
              name="interests"
              value={interest}
              checked={formData.interests.includes(interest)}
              onChange={handleInputChange}
            />
            {interest}
          </label>
        ))}
      </div>

      {/* Textarea */}
      <div>
        <label htmlFor="bio">Bio:</label>
        <textarea
          id="bio"
          name="bio"
          value={formData.bio}
          onChange={handleInputChange}
          rows="4"
          cols="50"
        />
      </div>

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

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

Form Validation

Basic Client-Side Validation

import React, { useState } from 'react';

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

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

  // Validation rules
  const validateField = (name, value) => {
    switch (name) {
      case 'username':
        if (!value.trim()) return 'Username is required';
        if (value.length < 3) return 'Username must be at least 3 characters';
        if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores';
        return '';

      case 'email':
        if (!value.trim()) return 'Email is required';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
        return '';

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

      case 'confirmPassword':
        if (!value) return 'Please confirm your password';
        if (value !== formData.password) return 'Passwords do not match';
        return '';

      default:
        return '';
    }
  };

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

    // Real-time validation
    if (touched[name]) {
      const error = validateField(name, value);
      setErrors(prev => ({
        ...prev,
        [name]: error
      }));
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;
   
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));

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

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

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

    setErrors(newErrors);
    setTouched({
      username: true,
      email: true,
      password: true,
      confirmPassword: true
    });

    if (Object.keys(newErrors).length === 0) {
      console.log('Form is valid:', formData);
      // Submit form data
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleInputChange}
          onBlur={handleBlur}
          className={errors.username ? 'error' : ''}
        />
        {errors.username && <span className="error-message">{errors.username}</span>}
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          onBlur={handleBlur}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleInputChange}
          onBlur={handleBlur}
          className={errors.password ? 'error' : ''}
        />
        {errors.password && <span className="error-message">{errors.password}</span>}
      </div>

      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleInputChange}
          onBlur={handleBlur}
          className={errors.confirmPassword ? 'error' : ''}
        />
        {errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
      </div>

      <button type="submit" disabled={Object.keys(errors).some(key => errors[key])}>
        Register
      </button>
    </form>
  );
}

Custom Form Hooks

useForm Hook

import { useState } from 'react';
function useForm(initialValues, validationRules) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const validate = (name, value) => {
    if (validationRules[name]) {
      return validationRules[name](value, values);
    }
    return '';
  };

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

    setValues(prev => ({
      ...prev,
      [name]: newValue
    }));

    if (touched[name]) {
      const error = validate(name, newValue);
      setErrors(prev => ({
        ...prev,
        [name]: error
      }));
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;
   
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));

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

  const validateAll = () => {
    const newErrors = {};
    const newTouched = {};

    Object.keys(values).forEach(key => {
      newTouched[key] = true;
      const error = validate(key, values[key]);
      if (error) newErrors[key] = error;
    });

    setTouched(newTouched);
    setErrors(newErrors);

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

  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validateAll,
    reset,
    isValid: Object.keys(errors).length === 0
  };
}

// Usage of useForm hook
function RegistrationForm() {
  const initialValues = {
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  };

  const validationRules = {
    username: (value) => {
      if (!value.trim()) return 'Username is required';
      if (value.length < 3) return 'Username must be at least 3 characters';
      return '';
    },
    email: (value) => {
      if (!value.trim()) return 'Email is required';
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
      return '';
    },
    password: (value) => {
      if (!value) return 'Password is required';
      if (value.length < 8) return 'Password must be at least 8 characters';
      return '';
    },
    confirmPassword: (value, allValues) => {
      if (!value) return 'Please confirm your password';
      if (value !== allValues.password) return 'Passwords do not match';
      return '';
    }
  };

  const form = useForm(initialValues, validationRules);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (form.validateAll()) {
      console.log('Form submitted:', form.values);
      form.reset();
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          type="text"
          id="username"
          name="username"
          value={form.values.username}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.username && form.touched.username && (
          <span className="error">{form.errors.username}</span>
        )}
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={form.values.email}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.email && form.touched.email && (
          <span className="error">{form.errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={form.values.password}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.password && form.touched.password && (
          <span className="error">{form.errors.password}</span>
        )}
      </div>

      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          type="password"
          id="confirmPassword"
          name="confirmPassword"
          value={form.values.confirmPassword}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
        />
        {form.errors.confirmPassword && form.touched.confirmPassword && (
          <span className="error">{form.errors.confirmPassword}</span>
        )}
      </div>

      <button type="submit" disabled={!form.isValid}>
        Register
      </button>
      <button type="button" onClick={form.reset}>
        Reset
      </button>
    </form>
  );
}

Third-Party Form Libraries

Formik

Formik is a popular library for building forms in React:

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

const validationSchema = Yup.object({
  firstName: Yup.string()
    .max(15, 'Must be 15 characters or less')
    .required('Required'),
  lastName: Yup.string()
    .max(20, 'Must be 20 characters or less')
    .required('Required'),
  email: Yup.string()
    .email('Invalid email address')
    .required('Required'),
});

function FormikExample() {
  return (
    <Formik
      initialValues={{
        firstName: '',
        lastName: '',
        email: '',
      }}
      validationSchema={validationSchema}
      onSubmit={(values, { setSubmitting, resetForm }) => {
        setTimeout(() => {
          console.log('Form submitted:', values);
          setSubmitting(false);
          resetForm();
        }, 1000);
      }}
    >
      {({ 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 Address</label>
            <Field name="email" type="email" />
            <ErrorMessage name="email" component="div" className="error" />
          </div>

          <button type="submit" disabled={isSubmitting}>
            Submit
          </button>
        </Form>
      )}
    </Formik>
  );
}

React Hook Form

React Hook Form is another excellent library for form handling:

npm install react-hook-form
import React from 'react';
import { useForm } from 'react-hook-form';

function ReactHookFormExample() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
    reset
  } = useForm();

  const onSubmit = (data) => {
    console.log('Form submitted:', data);
    reset();
  };

  const password = watch('password');

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          id="username"
          {...register('username', {
            required: 'Username is required',
            minLength: {
              value: 3,
              message: 'Username must be at least 3 characters'
            }
          })}
        />
        {errors.username && <span className="error">{errors.username.message}</span>}
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Please enter a valid email'
            }
          })}
        />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label htmlFor="password">Password:</label>
        <input
          id="password"
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 8,
              message: 'Password must be at least 8 characters'
            }
          })}
        />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>

      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          id="confirmPassword"
          type="password"
          {...register('confirmPassword', {
            required: 'Please confirm your password',
            validate: value => value === password || 'Passwords do not match'
          })}
        />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
      </div>

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

Advanced Form Patterns

Dynamic Forms

import React, { useState } from 'react';

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

  const addField = () => {
    const newId = Math.max(...fields.map(f => f.id)) + 1;
    setFields([...fields, { id: newId, name: '', email: '' }]);
  };

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

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

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

  return (
    <form onSubmit={handleSubmit}>
      <h3>Contact List</h3>
      {fields.map((field, index) => (
        <div key={field.id} style={{ border: '1px solid #ccc', padding: '10px', margin: '10px 0' }}>
          <h4>Contact {index + 1}</h4>
          <div>
            <label>Name:</label>
            <input
              type="text"
              value={field.name}
              onChange={(e) => updateField(field.id, 'name', e.target.value)}
              required
            />
          </div>
          <div>
            <label>Email:</label>
            <input
              type="email"
              value={field.email}
              onChange={(e) => updateField(field.id, 'email', e.target.value)}
              required
            />
          </div>
          <button
            type="button"
            onClick={() => removeField(field.id)}
            disabled={fields.length === 1}
          >
            Remove
          </button>
        </div>
      ))}
     
      <button type="button" onClick={addField}>
        Add Contact
      </button>
      <button type="submit">
        Submit All
      </button>
    </form>
  );
}

Multi-Step Forms

import React, { useState } from 'react';

function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(1);
  const [formData, setFormData] = useState({
    // Step 1
    firstName: '',
    lastName: '',
    email: '',
    // Step 2
    address: '',
    city: '',
    zipCode: '',
    // Step 3
    cardNumber: '',
    expiryDate: '',
    cvv: ''
  });

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

  const validateStep = (step) => {
    const newErrors = {};

    switch (step) {
      case 1:
        if (!formData.firstName.trim()) newErrors.firstName = 'First name is required';
        if (!formData.lastName.trim()) newErrors.lastName = 'Last name is required';
        if (!formData.email.trim()) newErrors.email = 'Email is required';
        break;
      case 2:
        if (!formData.address.trim()) newErrors.address = 'Address is required';
        if (!formData.city.trim()) newErrors.city = 'City is required';
        if (!formData.zipCode.trim()) newErrors.zipCode = 'Zip code is required';
        break;
      case 3:
        if (!formData.cardNumber.trim()) newErrors.cardNumber = 'Card number is required';
        if (!formData.expiryDate.trim()) newErrors.expiryDate = 'Expiry date is required';
        if (!formData.cvv.trim()) newErrors.cvv = 'CVV is required';
        break;
    }

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

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

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

  const prevStep = () => {
    setCurrentStep(prev => prev - 1);
  };

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

  const renderStep = () => {
    switch (currentStep) {
      case 1:
        return (
          <div>
            <h3>Personal Information</h3>
            <div>
              <label>First Name:</label>
              <input
                type="text"
                name="firstName"
                value={formData.firstName}
                onChange={handleInputChange}
              />
              {errors.firstName && <span className="error">{errors.firstName}</span>}
            </div>
            <div>
              <label>Last Name:</label>
              <input
                type="text"
                name="lastName"
                value={formData.lastName}
                onChange={handleInputChange}
              />
              {errors.lastName && <span className="error">{errors.lastName}</span>}
            </div>
            <div>
              <label>Email:</label>
              <input
                type="email"
                name="email"
                value={formData.email}
                onChange={handleInputChange}
              />
              {errors.email && <span className="error">{errors.email}</span>}
            </div>
          </div>
        );

      case 2:
        return (
          <div>
            <h3>Address Information</h3>
            <div>
              <label>Address:</label>
              <input
                type="text"
                name="address"
                value={formData.address}
                onChange={handleInputChange}
              />
              {errors.address && <span className="error">{errors.address}</span>}
            </div>
            <div>
              <label>City:</label>
              <input
                type="text"
                name="city"
                value={formData.city}
                onChange={handleInputChange}
              />
              {errors.city && <span className="error">{errors.city}</span>}
            </div>
            <div>
              <label>Zip Code:</label>
              <input
                type="text"
                name="zipCode"
                value={formData.zipCode}
                onChange={handleInputChange}
              />
              {errors.zipCode && <span className="error">{errors.zipCode}</span>}
            </div>
          </div>
        );

      case 3:
        return (
          <div>
            <h3>Payment Information</h3>
            <div>
              <label>Card Number:</label>
              <input
                type="text"
                name="cardNumber"
                value={formData.cardNumber}
                onChange={handleInputChange}
              />
              {errors.cardNumber && <span className="error">{errors.cardNumber}</span>}
            </div>
            <div>
              <label>Expiry Date:</label>
              <input
                type="text"
                name="expiryDate"
                value={formData.expiryDate}
                onChange={handleInputChange}
                placeholder="MM/YY"
              />
              {errors.expiryDate && <span className="error">{errors.expiryDate}</span>}
            </div>
            <div>
              <label>CVV:</label>
              <input
                type="text"
                name="cvv"
                value={formData.cvv}
                onChange={handleInputChange}
              />
              {errors.cvv && <span className="error">{errors.cvv}</span>}
            </div>
          </div>
        );

      default:
        return null;
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div className="step-indicator">
        Step {currentStep} of 3
      </div>
     
      {renderStep()}
     
      <div className="form-navigation">
        {currentStep > 1 && (
          <button type="button" onClick={prevStep}>
            Previous
          </button>
        )}
       
        {currentStep < 3 ? (
          <button type="button" onClick={nextStep}>
            Next
          </button>
        ) : (
          <button type="submit">
            Complete Order
          </button>
        )}
      </div>
    </form>
  );
}

Form Accessibility

ARIA Labels and Descriptions

import React, { useState } from 'react';

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

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

  return (
    <form role="form" aria-labelledby="form-title">
      <h2 id="form-title">User Registration</h2>
     
      <div>
        <label htmlFor="username">
          Username
          <span aria-label="required" className="required">*</span>
        </label>
        <input
          type="text"
          id="username"
          name="username"
          value={formData.username}
          onChange={handleInputChange}
          aria-required="true"
          aria-describedby="username-help username-error"
          aria-invalid={errors.username ? 'true' : 'false'}
        />
        <div id="username-help" className="help-text">
          Username must be 3-20 characters long
        </div>
        {errors.username && (
          <div id="username-error" className="error" role="alert">
            {errors.username}
          </div>
        )}
      </div>

      <div>
        <label htmlFor="email">
          Email Address
          <span aria-label="required" className="required">*</span>
        </label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          aria-required="true"
          aria-describedby="email-help email-error"
          aria-invalid={errors.email ? 'true' : 'false'}
        />
        <div id="email-help" className="help-text">
          We'll never share your email with anyone else
        </div>
        {errors.email && (
          <div id="email-error" className="error" role="alert">
            {errors.email}
          </div>
        )}
      </div>

      <div>
        <label htmlFor="password">
          Password
          <span aria-label="required" className="required">*</span>
        </label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleInputChange}
          aria-required="true"
          aria-describedby="password-help password-error"
          aria-invalid={errors.password ? 'true' : 'false'}
        />
        <div id="password-help" className="help-text">
          Password must contain at least 8 characters, including uppercase, lowercase, and numbers
        </div>
        {errors.password && (
          <div id="password-error" className="error" role="alert">
            {errors.password}
          </div>
        )}
      </div>

      <button type="submit" aria-describedby="submit-help">
        Create Account
      </button>
      <div id="submit-help" className="help-text">
        By clicking "Create Account", you agree to our terms of service
      </div>
    </form>
  );
}

File Upload Forms

Basic File Upload

import React, { useState } from 'react';

function FileUploadForm() {
  const [selectedFiles, setSelectedFiles] = useState([]);
  const [uploadProgress, setUploadProgress] = useState({});
  const [uploadStatus, setUploadStatus] = useState('');

  const handleFileSelect = (e) => {
    const files = Array.from(e.target.files);
    setSelectedFiles(files);
   
    // Initialize progress for each file
    const progress = {};
    files.forEach((file, index) => {
      progress[index] = 0;
    });
    setUploadProgress(progress);
  };

  const handleFileUpload = async (e) => {
    e.preventDefault();
   
    if (selectedFiles.length === 0) {
      setUploadStatus('Please select files to upload');
      return;
    }

    setUploadStatus('Uploading...');

    try {
      for (let i = 0; i < selectedFiles.length; i++) {
        const file = selectedFiles[i];
        const formData = new FormData();
        formData.append('file', file);

        // Simulate upload progress
        const uploadPromise = new Promise((resolve) => {
          let progress = 0;
          const interval = setInterval(() => {
            progress += 10;
            setUploadProgress(prev => ({
              ...prev,
              [i]: progress
            }));
           
            if (progress >= 100) {
              clearInterval(interval);
              resolve();
            }
          }, 200);
        });

        await uploadPromise;
      }
     
      setUploadStatus('All files uploaded successfully!');
    } catch (error) {
      setUploadStatus('Upload failed: ' + error.message);
    }
  };

  const removeFile = (index) => {
    const newFiles = selectedFiles.filter((_, i) => i !== index);
    setSelectedFiles(newFiles);
   
    const newProgress = { ...uploadProgress };
    delete newProgress[index];
    setUploadProgress(newProgress);
  };

  const formatFileSize = (bytes) => {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  };

  return (
    <form onSubmit={handleFileUpload}>
      <div>
        <label htmlFor="fileInput">Select Files:</label>
        <input
          type="file"
          id="fileInput"
          multiple
          onChange={handleFileSelect}
          accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx"
        />
      </div>

      {selectedFiles.length > 0 && (
        <div className="file-list">
          <h3>Selected Files:</h3>
          {selectedFiles.map((file, index) => (
            <div key={index} className="file-item">
              <div className="file-info">
                <span className="file-name">{file.name}</span>
                <span className="file-size">({formatFileSize(file.size)})</span>
                <button
                  type="button"
                  onClick={() => removeFile(index)}
                  className="remove-file"
                >
                  Remove
                </button>
              </div>
             
              {uploadProgress[index] !== undefined && (
                <div className="progress-bar">
                  <div
                    className="progress-fill"
                    style={{ width: `${uploadProgress[index]}%` }}
                  />
                  <span className="progress-text">{uploadProgress[index]}%</span>
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      <button type="submit" disabled={selectedFiles.length === 0}>
        Upload Files
      </button>

      {uploadStatus && (
        <div className={`upload-status ${uploadStatus.includes('success') ? 'success' : 'info'}`}>
          {uploadStatus}
        </div>
      )}
    </form>
  );
}

Drag and Drop File Upload

import React, { useState, useEffect, useCallback } from 'react';
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

function DebouncedSearchForm() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
 
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  // Mock API call
  const searchAPI = useCallback(async (query) => {
    setLoading(true);
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 300));
   
    const mockResults = [
      `Result 1 for "${query}"`,
      `Result 2 for "${query}"`,
      `Result 3 for "${query}"`
    ];
   
    setResults(mockResults);
    setLoading(false);
  }, []);

  useEffect(() => {
    if (debouncedSearchTerm) {
      searchAPI(debouncedSearchTerm);
    } else {
      setResults([]);
    }
  }, [debouncedSearchTerm, searchAPI]);

  return (
    <div>
      <form>
        <div>
          <label htmlFor="search">Search:</label>
          <input
            type="text"
            id="search"
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            placeholder="Type to search..."
          />
        </div>
      </form>

      {loading && <div>Searching...</div>}
     
      {results.length > 0 && (
        <div className="search-results">
          <h3>Search Results:</h3>
          <ul>
            {results.map((result, index) => (
              <li key={index}>{result}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

Form Performance Optimization

Debounced Input

import React, { useState, useEffect, useCallback } from 'react';
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

function DebouncedSearchForm() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
 
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  // Mock API call
  const searchAPI = useCallback(async (query) => {
    setLoading(true);
    // Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 300));
   
    const mockResults = [
      `Result 1 for "${query}"`,
      `Result 2 for "${query}"`,
      `Result 3 for "${query}"`
    ];
   
    setResults(mockResults);
    setLoading(false);
  }, []);

  useEffect(() => {
    if (debouncedSearchTerm) {
      searchAPI(debouncedSearchTerm);
    } else {
      setResults([]);
    }
  }, [debouncedSearchTerm, searchAPI]);

  return (
    <div>
      <form>
        <div>
          <label htmlFor="search">Search:</label>
          <input
            type="text"
            id="search"
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            placeholder="Type to search..."
          />
        </div>
      </form>

      {loading && <div>Searching...</div>}
     
      {results.length > 0 && (
        <div className="search-results">
          <h3>Search Results:</h3>
          <ul>
            {results.map((result, index) => (
              <li key={index}>{result}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

Best Practices and Common Patterns

Form State Management Tips

  1. Use controlled components for better state management
  2. Implement proper validation at both client and server levels
  3. Provide clear error messages that help users understand what went wrong
  4. Use proper ARIA attributes for accessibility
  5. Debounce expensive operations like API calls during typing
  6. Handle loading states to provide feedback during form submission
  7. Reset forms after successful submission
  8. Sanitize and validate all user inputs before processing

Common Form Patterns Summary

// 1. Basic controlled form
const [formData, setFormData] = useState(initialState);

// 2. Generic input handler
const handleInputChange = (e) => {
  const { name, value, type, checked } = e.target;
  setFormData(prev => ({
    ...prev,
    [name]: type === 'checkbox' ? checked : value
  }));
};

// 3. Form validation
const validateForm = () => {
  const errors = {};
  // Add validation logic
  return errors;
};

// 4. Form submission
const handleSubmit = async (e) => {
  e.preventDefault();
  const errors = validateForm();
  if (Object.keys(errors).length === 0) {
    // Submit form
  }
};

Testing Forms

Testing Form Components

Summary

Forms are essential components in React applications that require careful consideration of state management, validation, accessibility, and user experience. Key takeaways from this chapter include:

  • Controlled components provide better state management and validation capabilities
  • Proper validation should be implemented both client-side and server-side
  • Accessibility is crucial for inclusive forms using ARIA attributes and semantic HTML
  • Third-party libraries like Formik and React Hook Form can simplify complex form handling
  • Performance optimization techniques like debouncing improve user experience
  • Testing ensures forms work correctly and handle edge cases properly

The next chapter will explore React with TypeScript, showing how to add type safety to your React applications for better development experience and fewer runtime errors.

Related Articles

Scroll to Top