Skip to main content

Form Handling in React

Created: May 8, 2026 Larry Qu 7 min read

Form handling is crucial for interactive applications. This article covers form patterns and best practices.

Introduction

Form handling provides:

  • Controlled components
  • Form validation
  • Error handling
  • State management
  • User feedback

Understanding form handling helps you:

  • Build interactive forms
  • Validate user input
  • Handle form submission
  • Manage form state
  • Improve user experience

Controlled Components

Basic Controlled Forms

import { useState } from 'react';

// ✅ Good: Controlled input
function TextInput() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Enter text"
    />
  );
}

// ✅ Good: Controlled form
function LoginForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    rememberMe: false
  });

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

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

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <label>
        <input
          type="checkbox"
          name="rememberMe"
          checked={formData.rememberMe}
          onChange={handleChange}
        />
        Remember me
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

// ✅ Good: Select and textarea
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    country: '',
    message: ''
  });

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

  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <select
        name="country"
        value={formData.country}
        onChange={handleChange}
      >
        <option value="">Select country</option>
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
        <option value="ca">Canada</option>
      </select>
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
    </form>
  );
}

Form Validation

// ✅ Good: Basic validation
function RegistrationForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [errors, setErrors] = useState({});

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

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match';
    }

    return newErrors;
  };

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

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

    if (Object.keys(newErrors).length === 0) {
      console.log('Form is valid, submitting...');
    } else {
      setErrors(newErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <p className="error">{errors.email}</p>}
      </div>
      <div>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <p className="error">{errors.password}</p>}
      </div>
      <div>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
          placeholder="Confirm Password"
        />
        {errors.confirmPassword && <p className="error">{errors.confirmPassword}</p>}
      </div>
      <button type="submit">Register</button>
    </form>
  );
}

// ✅ Good: Real-time validation
function EmailInput() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

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

    if (!value) {
      setError('Email is required');
    } else if (!/\S+@\S+\.\S+/.test(value)) {
      setError('Email is invalid');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        placeholder="Email"
      />
      {error && <p className="error">{error}</p>}
    </div>
  );
}

React Hook Form

Basic React Hook Form

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

// ✅ Good: React Hook Form setup
function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      email: '',
      password: ''
    }
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: 'Email is invalid'
          }
        })}
        placeholder="Email"
      />
      {errors.email && <p className="error">{errors.email.message}</p>}

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

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

// ✅ Good: Dynamic fields
function DynamicForm() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  const watchedFields = watch();

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('firstName', { required: 'First name is required' })}
        placeholder="First name"
      />
      {errors.firstName && <p>{errors.firstName.message}</p>}

      <input
        {...register('lastName', { required: 'Last name is required' })}
        placeholder="Last name"
      />
      {errors.lastName && <p>{errors.lastName.message}</p>}

      <p>Full name: {watchedFields.firstName} {watchedFields.lastName}</p>

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

// ✅ Good: Conditional fields
function ConditionalForm() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  const accountType = watch('accountType');

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <select {...register('accountType')}>
        <option value="">Select account type</option>
        <option value="personal">Personal</option>
        <option value="business">Business</option>
      </select>

      {accountType === 'business' && (
        <input
          {...register('companyName', { required: 'Company name is required' })}
          placeholder="Company name"
        />
      )}

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

Advanced React Hook Form

import { useForm, useFieldArray, Controller } from 'react-hook-form';

// ✅ Good: Array fields
function MultipleEmailsForm() {
  const { register, handleSubmit, control, formState: { errors } } = useForm({
    defaultValues: {
      emails: [{ value: '' }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'emails'
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input
            {...register(`emails.${index}.value`, {
              required: 'Email is required'
            })}
            placeholder="Email"
          />
          {errors.emails?.[index]?.value && (
            <p>{errors.emails[index].value.message}</p>
          )}
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => append({ value: '' })}>
        Add Email
      </button>
      <button type="submit">Submit</button>
    </form>
  );
}

// ✅ Good: Custom validation
function CustomValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const validateUsername = async (value) => {
    const response = await fetch(`/api/check-username?username=${value}`);
    const data = await response.json();
    return data.available || 'Username is already taken';
  };

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input
        {...register('username', {
          required: 'Username is required',
          validate: validateUsername
        })}
        placeholder="Username"
      />
      {errors.username && <p>{errors.username.message}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

// ✅ Good: Controller for custom components
function CustomComponentForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="date"
        control={control}
        render={({ field }) => (
          <input type="date" {...field} />
        )}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Formik

Basic Formik Setup

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

// ✅ Good: Formik with validation schema
const validationSchema = Yup.object().shape({
  email: Yup.string()
    .email('Invalid email')
    .required('Email is required'),
  password: Yup.string()
    .min(8, 'Password must be at least 8 characters')
    .required('Password is required')
});

function LoginForm() {
  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={validationSchema}
      onSubmit={(values) => {
        console.log('Form submitted:', values);
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <Field
            type="email"
            name="email"
            placeholder="Email"
          />
          <ErrorMessage name="email" component="p" />

          <Field
            type="password"
            name="password"
            placeholder="Password"
          />
          <ErrorMessage name="password" component="p" />

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

// ✅ Good: Custom field component
function CustomField({ label, ...props }) {
  return (
    <div>
      <label>{label}</label>
      <Field {...props} />
      <ErrorMessage name={props.name} component="p" />
    </div>
  );
}

function RegistrationForm() {
  return (
    <Formik
      initialValues={{ name: '', email: '', password: '' }}
      validationSchema={validationSchema}
      onSubmit={(values) => console.log(values)}
    >
      <Form>
        <CustomField label="Name" name="name" type="text" />
        <CustomField label="Email" name="email" type="email" />
        <CustomField label="Password" name="password" type="password" />
        <button type="submit">Register</button>
      </Form>
    </Formik>
  );
}

Best Practices

  1. Use controlled components:
    // ✅ Good: Controlled
    const [value, setValue] = useState('');
    <input value={value} onChange={(e) => setValue(e.target.value)} />
    
    // ❌ Bad: Uncontrolled
    <input defaultValue="initial" />
    ```javascript
    
  2. Validate on submit and change:
    // ✅ Good: Validate on both
    const handleChange = (e) => {
      setValue(e.target.value);
      validateField(e.target.value);
    };
    
    // ❌ Bad: Only on submit
    const handleSubmit = () => {
      validateForm();
    };
    ```javascript
    
  3. Use form libraries for complex forms:
    // ✅ Good: React Hook Form for complex forms
    const { register, handleSubmit } = useForm();
    
    // ❌ Bad: Manual state for complex forms
    const [formData, setFormData] = useState({...});
    

Summary

Form handling is essential. Key takeaways:

  • Use controlled components
  • Validate user input
  • Provide clear error messages
  • Use React Hook Form for performance
  • Use Formik for complex forms
  • Handle form submission properly
  • Improve user experience

Next Steps

Resources

Comments

Share this article

Scan to read on mobile