Skip to main content
โšก Calmops

Form Handling in React

Form Handling in React

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" />
    
  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();
    };
    
  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

Comments