Form Handling in React

Creating, managing, and validating forms in React applications

Introduction to Forms in React

Forms are a fundamental part of web applications, allowing users to input and submit data. In React, forms are slightly different from traditional HTML forms because the data is typically controlled by React components rather than the DOM itself.

Real-world analogy: Think of a React form like a digital survey with an assistant. In a paper survey, you fill in answers and submit the whole thing at once. With the digital survey, the assistant (React) watches what you type in real-time, can validate your answers immediately, and can even change questions based on previous answers.

flowchart TD A[User Input] --> B[React Component State] B --> C[Rendered Form Elements] C --> A B --> D[Form Submission] D --> E[Data Processing/API Calls] style B fill:#f9f,stroke:#333,stroke-width:2px

Key Concepts in React Forms

Controlled Components

In HTML, form elements like <input>, <textarea>, and <select> maintain their own state and update based on user input. In React, we typically want to have all state managed by React.

A controlled component is a form element whose value is controlled by React state. Every state change is handled through a handler function, giving you complete control over the form.

Basic Controlled Input


import React, { useState } from 'react';

function NameInput() {
  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">Greet Me</button>
    </form>
  );
}
      

In this example:

  1. The name state stores the current input value
  2. We set the input's value attribute to the name state
  3. The onChange handler updates the state when the user types
  4. The form submission is handled by handleSubmit
sequenceDiagram participant User participant Input participant React participant State User->>Input: Types "John" Input->>React: onChange event React->>State: Update state to "John" State->>Input: Re-render with value="John" User->>Input: Clicks Submit Input->>React: onSubmit event React->>React: Process form data

Working with Different Form Elements

Text and Textarea Inputs

Both <input type="text"> and <textarea> work similarly in React:


function TextForm() {
  const [inputValue, setInputValue] = useState('');
  const [textareaValue, setTextareaValue] = useState('');
  
  return (
    <form>
      <div>
        <label htmlFor="simple-input">Simple Input:</label>
        <input
          id="simple-input"
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
      </div>
      
      <div>
        <label htmlFor="textarea-input">Textarea:</label>
        <textarea
          id="textarea-input"
          value={textareaValue}
          onChange={(e) => setTextareaValue(e.target.value)}
          rows="4"
        />
      </div>
      
      <div>
        <p>Input value: {inputValue}</p>
        <p>Textarea value: {textareaValue}</p>
      </div>
    </form>
  );
}
      

Note: In HTML, a <textarea> defines its content between tags. In React, a <textarea> uses a value attribute instead, making it work more consistently with other form elements.

Checkbox and Radio Inputs

For checkboxes and radio buttons, we use the checked attribute instead of value:


function PreferenceForm() {
  const [isSubscribed, setIsSubscribed] = useState(false);
  const [plan, setPlan] = useState('free');
  
  return (
    <form>
      <div>
        <label>
          <input
            type="checkbox"
            checked={isSubscribed}
            onChange={(e) => setIsSubscribed(e.target.checked)}
          />
          Subscribe to newsletter
        </label>
      </div>
      
      <div>
        <p>Subscription Plan:</p>
        
        <label>
          <input
            type="radio"
            value="free"
            checked={plan === 'free'}
            onChange={(e) => setPlan(e.target.value)}
          />
          Free
        </label>
        
        <label>
          <input
            type="radio"
            value="pro"
            checked={plan === 'pro'}
            onChange={(e) => setPlan(e.target.value)}
          />
          Pro
        </label>
        
        <label>
          <input
            type="radio"
            value="enterprise"
            checked={plan === 'enterprise'}
            onChange={(e) => setPlan(e.target.value)}
          />
          Enterprise
        </label>
      </div>
      
      <div>
        <p>You are {isSubscribed ? 'subscribed' : 'not subscribed'} to the newsletter.</p>
        <p>Your plan: {plan}</p>
      </div>
    </form>
  );
}
      

Select Dropdowns

The <select> element also works as a controlled component in React:


function CountrySelector() {
  const [country, setCountry] = useState('');
  
  return (
    <form>
      <label htmlFor="country">Select your country:</label>
      <select
        id="country"
        value={country}
        onChange={(e) => setCountry(e.target.value)}
      >
        <option value="">-- Select a country --</option>
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="mx">Mexico</option>
        <option value="uk">United Kingdom</option>
        <option value="fr">France</option>
      </select>
      
      {country && (
        <p>You selected: {country}</p>
      )}
    </form>
  );
}
      

Multiple Select

For a <select> with multiple attribute, we handle an array of values:


function SkillsSelector() {
  const [skills, setSkills] = useState([]);
  
  const handleSkillChange = (event) => {
    const selectedOptions = Array.from(event.target.selectedOptions, 
      option => option.value);
    setSkills(selectedOptions);
  };
  
  return (
    <form>
      <label htmlFor="skills">Select your skills (hold Ctrl/Cmd to select multiple):</label>
      <select
        id="skills"
        multiple
        value={skills}
        onChange={handleSkillChange}
        style={{ height: '120px' }}
      >
        <option value="javascript">JavaScript</option>
        <option value="react">React</option>
        <option value="nodejs">Node.js</option>
        <option value="python">Python</option>
        <option value="java">Java</option>
        <option value="csharp">C#</option>
      </select>
      
      {skills.length > 0 && (
        <div>
          <p>Your skills:</p>
          <ul>
            {skills.map(skill => (
              <li key={skill}>{skill}</li>
            ))}
          </ul>
        </div>
      )}
    </form>
  );
}
      

Handling Multiple Form Fields

For forms with multiple fields, managing separate state for each field can be cumbersome. Instead, we can use a single state object to handle all form fields:


function RegistrationForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    age: '',
    gender: '',
    interests: []
  });
  
  const handleChange = (event) => {
    const { name, value, type, checked } = event.target;
    
    if (type === 'checkbox') {
      // Handle checkbox (interests)
      if (checked) {
        setFormData({
          ...formData,
          interests: [...formData.interests, value]
        });
      } else {
        setFormData({
          ...formData,
          interests: formData.interests.filter(interest => interest !== value)
        });
      }
    } else {
      // Handle all other input types
      setFormData({
        ...formData,
        [name]: value
      });
    }
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Form submitted:', formData);
    // Here you would typically send the data to a server
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name:</label>
        <input
          id="firstName"
          name="firstName"
          type="text"
          value={formData.firstName}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="lastName">Last Name:</label>
        <input
          id="lastName"
          name="lastName"
          type="text"
          value={formData.lastName}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="password">Password:</label>
        <input
          id="password"
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          required
          minLength="8"
        />
      </div>
      
      <div>
        <label htmlFor="age">Age:</label>
        <input
          id="age"
          name="age"
          type="number"
          value={formData.age}
          onChange={handleChange}
          min="18"
          max="120"
        />
      </div>
      
      <div>
        <p>Gender:</p>
        <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>
      
      <div>
        <p>Interests:</p>
        <label>
          <input
            type="checkbox"
            name="interests"
            value="technology"
            checked={formData.interests.includes('technology')}
            onChange={handleChange}
          />
          Technology
        </label>
        
        <label>
          <input
            type="checkbox"
            name="interests"
            value="sports"
            checked={formData.interests.includes('sports')}
            onChange={handleChange}
          />
          Sports
        </label>
        
        <label>
          <input
            type="checkbox"
            name="interests"
            value="music"
            checked={formData.interests.includes('music')}
            onChange={handleChange}
          />
          Music
        </label>
        
        <label>
          <input
            type="checkbox"
            name="interests"
            value="art"
            checked={formData.interests.includes('art')}
            onChange={handleChange}
          />
          Art
        </label>
      </div>
      
      <button type="submit">Register</button>
    </form>
  );
}
      

Key benefits of this approach:

Note: The key to making this work is using the name attribute on each input that matches the corresponding property in your state object.

Form Validation

Form validation is crucial for ensuring users submit correct and complete data. There are several approaches to validation in React forms:

Built-in HTML5 Validation

Modern browsers provide built-in validation using attributes like required, min, max, pattern, etc:


function HTML5ValidationForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    // Form will only submit if all validations pass
    alert('Form submitted successfully!');
  };
  
  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="username">Username (3-20 characters):</label>
        <input
          id="username"
          type="text"
          required
          minLength="3"
          maxLength="20"
          pattern="[A-Za-z0-9]+"
          title="Username can only contain letters and numbers"
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          required
        />
      </div>
      
      <div>
        <label htmlFor="age">Age (18+):</label>
        <input
          id="age"
          type="number"
          required
          min="18"
          max="120"
        />
      </div>
      
      <div>
        <label htmlFor="website">Website:</label>
        <input
          id="website"
          type="url"
          placeholder="https://example.com"
        />
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}
      

Note: The noValidate attribute lets you disable the browser's default validation UI while still being able to use the validation API programmatically.

Custom Validation in React

For more complex validation logic, or for a consistent UI across browsers, you can implement custom validation in React:


function CustomValidationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormData({
      ...formData,
      [name]: value
    });
  };
  
  const handleBlur = (event) => {
    const { name } = event.target;
    setTouched({
      ...touched,
      [name]: true
    });
    
    // Validate the field on blur
    validateField(name, formData[name]);
  };
  
  const validateField = (name, value) => {
    let newErrors = { ...errors };
    
    switch (name) {
      case 'username':
        if (!value) {
          newErrors.username = 'Username is required';
        } else if (value.length < 3) {
          newErrors.username = 'Username must be at least 3 characters';
        } else if (!/^[A-Za-z0-9]+$/.test(value)) {
          newErrors.username = 'Username can only contain letters and numbers';
        } else {
          delete newErrors.username;
        }
        break;
        
      case 'email':
        if (!value) {
          newErrors.email = 'Email is required';
        } else if (!/\S+@\S+\.\S+/.test(value)) {
          newErrors.email = 'Email address is invalid';
        } else {
          delete newErrors.email;
        }
        break;
        
      case 'password':
        if (!value) {
          newErrors.password = 'Password is required';
        } else if (value.length < 8) {
          newErrors.password = 'Password must be at least 8 characters';
        } else if (!/[A-Z]/.test(value)) {
          newErrors.password = 'Password must contain at least one uppercase letter';
        } else if (!/[0-9]/.test(value)) {
          newErrors.password = 'Password must contain at least one number';
        } else {
          delete newErrors.password;
        }
        
        // Also validate confirmPassword if it exists
        if (formData.confirmPassword && value !== formData.confirmPassword) {
          newErrors.confirmPassword = 'Passwords do not match';
        } else if (formData.confirmPassword) {
          delete newErrors.confirmPassword;
        }
        break;
        
      case 'confirmPassword':
        if (!value) {
          newErrors.confirmPassword = 'Please confirm your password';
        } else if (value !== formData.password) {
          newErrors.confirmPassword = 'Passwords do not match';
        } else {
          delete newErrors.confirmPassword;
        }
        break;
        
      default:
        break;
    }
    
    setErrors(newErrors);
  };
  
  const validateForm = () => {
    // Validate all fields
    Object.keys(formData).forEach(field => {
      validateField(field, formData[field]);
    });
    
    // Mark all fields as touched
    const allTouched = Object.keys(formData).reduce((acc, field) => {
      acc[field] = true;
      return acc;
    }, {});
    
    setTouched(allTouched);
    
    // Form is valid if there are no errors
    return Object.keys(errors).length === 0;
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    
    if (validateForm()) {
      // Form is valid, proceed with submission
      console.log('Form data:', formData);
      alert('Form submitted successfully!');
    } else {
      console.log('Form has errors:', errors);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="username">Username:</label>
        <input
          id="username"
          name="username"
          type="text"
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.username && errors.username ? 'error' : ''}
        />
        {touched.username && errors.username && (
          <div className="error-message">{errors.username}</div>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          name="email"
          type="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
          id="password"
          name="password"
          type="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>
      
      <div>
        <label htmlFor="confirmPassword">Confirm Password:</label>
        <input
          id="confirmPassword"
          name="confirmPassword"
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.confirmPassword && errors.confirmPassword ? 'error' : ''}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <div className="error-message">{errors.confirmPassword}</div>
        )}
      </div>
      
      <button type="submit">Register</button>
    </form>
  );
}
      

Key features of this validation approach:

Uncontrolled Components

While controlled components are the recommended way to handle form data in React, there are cases where using uncontrolled components can be simpler.

An uncontrolled component lets the DOM handle the form data internally, and you extract the value when needed (e.g., on form submission) using refs.


import React, { useRef } from 'react';

function UncontrolledForm() {
  const nameRef = useRef();
  const emailRef = useRef();
  const messageRef = useRef();
  
  const handleSubmit = (event) => {
    event.preventDefault();
    
    // Access form values using refs
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value,
      message: messageRef.current.value
    };
    
    console.log('Form submitted:', formData);
    
    // Reset the form
    event.target.reset();
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          type="text"
          ref={nameRef}
          defaultValue=""
          required
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          ref={emailRef}
          defaultValue=""
          required
        />
      </div>
      
      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          ref={messageRef}
          defaultValue=""
          rows="4"
          required
        />
      </div>
      
      <button type="submit">Send Message</button>
    </form>
  );
}
      

When to Use Uncontrolled Components

File Inputs

File inputs are always uncontrolled in React, since the value is read-only:


function FileUploadForm() {
  const fileInputRef = useRef();
  
  const handleSubmit = (event) => {
    event.preventDefault();
    
    const files = fileInputRef.current.files;
    console.log('Selected files:', files);
    
    // To upload files, you would typically use FormData
    const formData = new FormData();
    for (let i = 0; i < files.length; i++) {
      formData.append('files', files[i]);
    }
    
    // Then send formData to your server
    console.log('Ready to upload:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit} >
      <div>
        <label htmlFor="files">Select files:</label>
        <input
          id="files"
          type="file"
          ref={fileInputRef}
          multiple
        />
      </div>
      
      <button type="submit">Upload</button>
    </form>
  );
}
      

Form Libraries

For complex forms, using a form library can save time and reduce boilerplate. Here are some popular options:

Formik

Formik is one of the most popular form libraries for React, offering a complete solution:


import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup'; // For validation schema

function FormikExample() {
  // Define validation schema
  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'),
    acceptedTerms: Yup.boolean()
      .required('Required')
      .oneOf([true], 'You must accept the terms and conditions'),
    jobType: Yup.string()
      .oneOf(
        ['designer', 'developer', 'manager', 'other'],
        'Invalid job type'
      )
      .required('Required')
  });
  
  return (
    <div>
      <h2>Sign Up</h2>
      <Formik
        initialValues={{
          firstName: '',
          lastName: '',
          email: '',
          acceptedTerms: false,
          jobType: ''
        }}
        validationSchema={validationSchema}
        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="jobType">Job Type</label>
              <Field name="jobType" as="select">
                <option value="">Select a job type</option>
                <option value="designer">Designer</option>
                <option value="developer">Developer</option>
                <option value="manager">Product Manager</option>
                <option value="other">Other</option>
              </Field>
              <ErrorMessage name="jobType" component="div" className="error" />
            </div>
            
            <div>
              <label>
                <Field name="acceptedTerms" type="checkbox" />
                I accept the terms and conditions
              </label>
              <ErrorMessage name="acceptedTerms" component="div" className="error" />
            </div>
            
            <button type="submit" disabled={isSubmitting}>
              {isSubmitting ? 'Submitting...' : 'Submit'}
            </button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
      

React Hook Form

React Hook Form is a lightweight library with a focus on performance:


import { useForm } from 'react-hook-form';

function HookFormExample() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors, isSubmitting }
  } = useForm();
  
  const onSubmit = async (data) => {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(data);
    alert('Form submitted!');
  };
  
  // Watch the password field for confirmation validation
  const password = watch('password');
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          {...register('name', { 
            required: 'Name is required',
            minLength: {
              value: 2,
              message: 'Name must be at least 2 characters'
            }
          })}
        />
        {errors.name && (
          <p className="error">{errors.name.message}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          {...register('email', { 
            required: 'Email is required',
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: 'Email address is invalid'
            }
          })}
        />
        {errors.email && (
          <p className="error">{errors.email.message}</p>
        )}
      </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 && (
          <p className="error">{errors.password.message}</p>
        )}
      </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 && (
          <p className="error">{errors.confirmPassword.message}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="age">Age:</label>
        <input
          id="age"
          type="number"
          {...register('age', { 
            required: 'Age is required',
            min: {
              value: 18,
              message: 'You must be at least 18 years old'
            },
            max: {
              value: 120,
              message: 'Age cannot exceed 120'
            }
          })}
        />
        {errors.age && (
          <p className="error">{errors.age.message}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="occupation">Occupation:</label>
        <select
          id="occupation"
          {...register('occupation', { 
            required: 'Please select an occupation'
          })}
        >
          <option value="">Select your occupation</option>
          <option value="developer">Developer</option>
          <option value="designer">Designer</option>
          <option value="manager">Manager</option>
          <option value="student">Student</option>
          <option value="other">Other</option>
        </select>
        {errors.occupation && (
          <p className="error">{errors.occupation.message}</p>
        )}
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            {...register('termsAccepted', { 
              required: 'You must accept the terms and conditions'
            })}
          />
          I accept the terms and conditions
        </label>
        {errors.termsAccepted && (
          <p className="error">{errors.termsAccepted.message}</p>
        )}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}
      

Comparing Form Libraries

Feature Formik React Hook Form Redux Form
Bundle Size Medium Small Large
Performance Good Excellent Average
Learning Curve Medium Low High
Form State Management Internal Uncontrolled Redux
Validation Built-in or Yup Built-in or Yup Custom

Real-World Applications

Dynamic Form with Field Array

A common requirement is to handle dynamic form fields, like a list of items:


function DynamicFormExample() {
  const [formValues, setFormValues] = useState({
    projectName: '',
    tasks: [{ title: '', description: '', completed: false }]
  });
  
  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormValues(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleTaskChange = (index, event) => {
    const { name, value, type, checked } = event.target;
    const newValue = type === 'checkbox' ? checked : value;
    
    const tasks = [...formValues.tasks];
    tasks[index] = {
      ...tasks[index],
      [name]: newValue
    };
    
    setFormValues(prev => ({
      ...prev,
      tasks
    }));
  };
  
  const handleAddTask = () => {
    setFormValues(prev => ({
      ...prev,
      tasks: [...prev.tasks, { title: '', description: '', completed: false }]
    }));
  };
  
  const handleRemoveTask = (index) => {
    const tasks = [...formValues.tasks];
    tasks.splice(index, 1);
    
    setFormValues(prev => ({
      ...prev,
      tasks
    }));
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Project data:', formValues);
    alert('Project saved!');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>Project Details</h2>
      
      <div>
        <label htmlFor="projectName">Project Name:</label>
        <input
          id="projectName"
          name="projectName"
          value={formValues.projectName}
          onChange={handleChange}
          required
        />
      </div>
      
      <h3>Tasks</h3>
      
      {formValues.tasks.map((task, index) => (
        <div key={index} className="task-item">
          <h4>Task #{index + 1}</h4>
          
          <div>
            <label htmlFor={`task-title-${index}`}>Title:</label>
            <input
              id={`task-title-${index}`}
              name="title"
              value={task.title}
              onChange={(e) => handleTaskChange(index, e)}
              required
            />
          </div>
          
          <div>
            <label htmlFor={`task-description-${index}`}>Description:</label>
            <textarea
              id={`task-description-${index}`}
              name="description"
              value={task.description}
              onChange={(e) => handleTaskChange(index, e)}
              rows="2"
            />
          </div>
          
          <div>
            <label>
              <input
                type="checkbox"
                name="completed"
                checked={task.completed}
                onChange={(e) => handleTaskChange(index, e)}
              />
              Completed
            </label>
          </div>
          
          {formValues.tasks.length > 1 && (
            <button 
              type="button" 
              onClick={() => handleRemoveTask(index)}
              className="remove-button"
            >
              Remove Task
            </button>
          )}
        </div>
      ))}
      
      <button 
        type="button" 
        onClick={handleAddTask}
        className="add-button"
      >
        Add Task
      </button>
      
      <div>
        <button type="submit" className="submit-button">
          Save Project
        </button>
      </div>
    </form>
  );
}
      

Multi-Step Form with Progress Tracking

For complex forms, breaking them into multiple steps improves user experience:


function MultiStepForm() {
  const [formData, setFormData] = useState({
    // Step 1: Personal Info
    firstName: '',
    lastName: '',
    email: '',
    
    // Step 2: Address
    street: '',
    city: '',
    state: '',
    zipCode: '',
    
    // Step 3: Account Settings
    username: '',
    password: '',
    notifications: false
  });
  
  const [currentStep, setCurrentStep] = useState(1);
  const totalSteps = 3;
  
  const handleChange = (event) => {
    const { name, value, type, checked } = event.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };
  
  const nextStep = () => {
    setCurrentStep(prev => Math.min(prev + 1, totalSteps));
  };
  
  const prevStep = () => {
    setCurrentStep(prev => Math.max(prev - 1, 1));
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    // Submit the form data
    console.log('Form submitted:', formData);
    alert('Registration complete!');
  };
  
  // Render different form steps
  const renderStep = () => {
    switch (currentStep) {
      case 1:
        return (
          <div className="form-step">
            <h3>Personal Information</h3>
            
            <div>
              <label htmlFor="firstName">First Name:</label>
              <input
                id="firstName"
                name="firstName"
                value={formData.firstName}
                onChange={handleChange}
                required
              />
            </div>
            
            <div>
              <label htmlFor="lastName">Last Name:</label>
              <input
                id="lastName"
                name="lastName"
                value={formData.lastName}
                onChange={handleChange}
                required
              />
            </div>
            
            <div>
              <label htmlFor="email">Email:</label>
              <input
                id="email"
                name="email"
                type="email"
                value={formData.email}
                onChange={handleChange}
                required
              />
            </div>
          </div>
        );
        
      case 2:
        return (
          <div className="form-step">
            <h3>Address Information</h3>
            
            <div>
              <label htmlFor="street">Street Address:</label>
              <input
                id="street"
                name="street"
                value={formData.street}
                onChange={handleChange}
                required
              />
            </div>
            
            <div>
              <label htmlFor="city">City:</label>
              <input
                id="city"
                name="city"
                value={formData.city}
                onChange={handleChange}
                required
              />
            </div>
            
            <div>
              <label htmlFor="state">State:</label>
              <select
                id="state"
                name="state"
                value={formData.state}
                onChange={handleChange}
                required
              >
                <option value="">Select a state</option>
                <option value="CA">California</option>
                <option value="NY">New York</option>
                <option value="TX">Texas</option>
                {/* More states... */}
              </select>
            </div>
            
            <div>
              <label htmlFor="zipCode">ZIP Code:</label>
              <input
                id="zipCode"
                name="zipCode"
                value={formData.zipCode}
                onChange={handleChange}
                required
                pattern="[0-9]{5}"
              />
            </div>
          </div>
        );
        
      case 3:
        return (
          <div className="form-step">
            <h3>Account Settings</h3>
            
            <div>
              <label htmlFor="username">Username:</label>
              <input
                id="username"
                name="username"
                value={formData.username}
                onChange={handleChange}
                required
                minLength="4"
              />
            </div>
            
            <div>
              <label htmlFor="password">Password:</label>
              <input
                id="password"
                name="password"
                type="password"
                value={formData.password}
                onChange={handleChange}
                required
                minLength="8"
              />
            </div>
            
            <div>
              <label>
                <input
                  type="checkbox"
                  name="notifications"
                  checked={formData.notifications}
                  onChange={handleChange}
                />
                Receive email notifications
              </label>
            </div>
          </div>
        );
        
      default:
        return null;
    }
  };
  
  return (
    <div className="multi-step-form">
      <div className="progress-bar">
        {[...Array(totalSteps)].map((_, index) => (
          <div 
            key={index}
            className={`progress-step ${currentStep > index ? 'completed' : ''} ${currentStep === index + 1 ? 'active' : ''}`}
            onClick={() => setCurrentStep(index + 1)}
          >
            {index + 1}
          </div>
        ))}
      </div>
      
      <form onSubmit={handleSubmit}>
        {renderStep()}
        
        <div className="form-navigation">
          {currentStep > 1 && (
            <button type="button" onClick={prevStep}>
              Previous
            </button>
          )}
          
          {currentStep < totalSteps ? (
            <button type="button" onClick={nextStep} >
              Next
            </button>
          ) : (
            <button type="submit" >
              Submit
            </button>
          )}
        </div>
      </form>
    </div>
  );
}
      

Practice Exercises

Exercise 1: Login Form

Create a login form with the following features:

Exercise 2: Registration Form

Build a user registration form that:

Exercise 3: Feedback Survey

Create a customer feedback survey form with:

Summary

In this lecture, we've covered:

Forms are a critical part of most web applications, and React provides powerful tools for creating interactive, user-friendly form experiences. By understanding these concepts, you can build forms that are both easy to use and maintainable.

Further Resources