React & Next.js Topic

React Forms & Controlled Components

1. Controlled Components

Each form input is tied to React state via value and onChange. The component fully controls the input.

Basic Controlled Inputs

jsx

import React, { useState } from 'react';

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

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

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

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

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

      {/* Password Input */}
      <div>
        <label>Password:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          minLength="6"
          required
        />
      </div>

      {/* Textarea */}
      <div>
        <label>Bio:</label>
        <textarea
          name="bio"
          value={formData.bio}
          onChange={handleChange}
          rows="4"
        />
      </div>

      {/* Select Dropdown */}
      <div>
        <label>Country:</label>
        <select
          name="country"
          value={formData.country}
          onChange={handleChange}
          required
        >
          <option value="">Select Country</option>
          <option value="us">United States</option>
          <option value="ca">Canada</option>
          <option value="uk">United Kingdom</option>
        </select>
      </div>

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

      {/* Radio Buttons */}
      <div>
        <label>Gender:</label>
        <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>
      </div>

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

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

2. Advanced Input Handling

Custom Hook for Form Management

jsx

import { useState, useCallback } from 'react';

function useForm(initialState, validations) {
  const [formData, setFormData] = useState(initialState);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

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

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

  const handleBlur = useCallback((e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    
    // Validate field on blur
    if (validations && validations[name]) {
      const error = validations[name](formData[name]);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [formData, validations]);

  const setFieldValue = useCallback((name, value) => {
    setFormData(prev => ({ ...prev, [name]: value }));
  }, []);

  const resetForm = useCallback(() => {
    setFormData(initialState);
    setErrors({});
    setTouched({});
  }, [initialState]);

  return {
    formData,
    errors,
    touched,
    handleChange,
    handleBlur,
    setFieldValue,
    resetForm,
    setFormData
  };
}

3. Validation Patterns

Client-Side Validation

jsx

import React, { useState } from 'react';

// Validation functions
const validations = {
  email: (value) => {
    if (!value) return 'Email is required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
    return '';
  },
  password: (value) => {
    if (!value) return 'Password is required';
    if (value.length < 6) return 'Password must be at least 6 characters';
    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) 
      return 'Password must contain uppercase, lowercase and number';
    return '';
  },
  confirmPassword: (value, allValues) => {
    if (!value) return 'Please confirm password';
    if (value !== allValues.password) return 'Passwords do not match';
    return '';
  }
};

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

  const validateField = (name, value) => {
    if (validations[name]) {
      return validations[name](value, formData);
    }
    return '';
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // Validate on change if field was already touched
    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 validateForm = () => {
    const newErrors = {};
    let isValid = true;

    Object.keys(formData).forEach(key => {
      const error = validateField(key, formData[key]);
      if (error) {
        newErrors[key] = error;
        isValid = false;
      }
    });

    setErrors(newErrors);
    setTouched(Object.keys(formData).reduce((acc, key) => ({
      ...acc, [key]: true
    }), {}));

    return isValid;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      console.log('Form is valid!', formData);
    } else {
      console.log('Form has errors');
    }
  };

  const getFieldClass = (name) => {
    return `form-field ${errors[name] ? 'error' : ''} ${touched[name] ? 'touched' : ''}`;
  };

  return (
    <form onSubmit={handleSubmit} className="form">
      <div className={getFieldClass('email')}>
        <label>Email:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.email ? 'input-error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>

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

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

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

4. React Hook Form

Uses uncontrolled components internally with refs, minimizing re-renders. State is managed by the library, not React.

Basic Usage

jsx

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

function ReactHookFormExample() {
  const { 
    register, 
    handleSubmit, 
    formState: { errors, isSubmitting, isValid, touchedFields },
    watch,
    reset,
    setValue,
    getValues
  } = useForm({
    mode: 'onChange', // Validate on change
    defaultValues: {
      firstName: '',
      email: '',
      age: 18
    }
  });

  const onSubmit = async (data) => {
    console.log('Form data:', data);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000));
    reset();
  };

  // Watch specific field
  const watchPassword = watch('password');

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Text Input with Validation */}
      <div>
        <label>First Name:</label>
        <input
          {...register('firstName', {
            required: 'First name is required',
            minLength: {
              value: 2,
              message: 'Minimum 2 characters'
            }
          })}
        />
        {errors.firstName && (
          <span className="error">{errors.firstName.message}</span>
        )}
      </div>

      {/* Email with Pattern Validation */}
      <div>
        <label>Email:</label>
        <input
          type="email"
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Invalid email address'
            }
          })}
        />
        {errors.email && (
          <span className="error">{errors.email.message}</span>
        )}
      </div>

      {/* Conditional Fields */}
      <div>
        <label>Create Password:</label>
        <input
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 6,
              message: 'Minimum 6 characters'
            }
          })}
        />
        {errors.password && (
          <span className="error">{errors.password.message}</span>
        )}
      </div>

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

      {/* Number Input */}
      <div>
        <label>Age:</label>
        <input
          type="number"
          {...register('age', {
            min: {
              value: 18,
              message: 'Must be at least 18 years old'
            },
            max: {
              value: 100,
              message: 'Must be less than 100'
            }
          })}
        />
        {errors.age && <span className="error">{errors.age.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting || !isValid}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

Advanced React Hook Form

jsx

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

function AdvancedReactHookForm() {
  const { 
    control,
    register,
    handleSubmit,
    formState: { errors },
    watch
  } = useForm({
    defaultValues: {
      users: [{ firstName: '', lastName: '' }],
      country: '',
      preferences: []
    }
  });

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

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Dynamic Fields */}
      <div>
        <h3>Users</h3>
        {fields.map((field, index) => (
          <div key={field.id}>
            <input
              {...register(`users.${index}.firstName`)}
              placeholder="First Name"
            />
            <input
              {...register(`users.${index}.lastName`)}
              placeholder="Last Name"
            />
            <button type="button" onClick={() => remove(index)}>
              Remove
            </button>
          </div>
        ))}
        <button type="button" onClick={() => append({ firstName: '', lastName: '' })}>
          Add User
        </button>
      </div>

      {/* Controlled Component with Controller */}
      <div>
        <label>Country:</label>
        <Controller
          name="country"
          control={control}
          render={({ field }) => (
            <Select
              {...field}
              options={[
                { value: 'us', label: 'United States' },
                { value: 'ca', label: 'Canada' },
                { value: 'uk', label: 'United Kingdom' }
              ]}
            />
          )}
        />
      </div>

      {/* File Input */}
      <div>
        <label>Profile Picture:</label>
        <input
          type="file"
          {...register('avatar', {
            validate: {
              lessThan1MB: (files) => 
                files[0]?.size < 1000000 || 'Max 1MB',
              acceptedFormats: (files) =>
                ['image/jpeg', 'image/png'].includes(files[0]?.type) || 
                'Only JPEG/PNG'
            }
          })}
        />
        {errors.avatar && <span className="error">{errors.avatar.message}</span>}
      </div>

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

5. Formik

Provides a higher-level abstraction for form state and validation. Feels similar to Redux patterns (centralized state).

Basic Formik Usage

jsx

import { useFormik } from 'formik';
import * as Yup from 'yup';

function FormikExample() {
  const formik = useFormik({
    initialValues: {
      email: '',
      password: '',
      rememberMe: false
    },
    validationSchema: Yup.object({
      email: Yup.string()
        .email('Invalid email address')
        .required('Required'),
      password: Yup.string()
        .min(6, 'Must be at least 6 characters')
        .required('Required')
    }),
    onSubmit: (values) => {
      console.log('Form data:', values);
    }
  });

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

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

      <div>
        <label>
          <input
            type="checkbox"
            name="rememberMe"
            checked={formik.values.rememberMe}
            onChange={formik.handleChange}
          />
          Remember me
        </label>
      </div>

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

6. Performance Optimization

Memoized Form Components

jsx

import React, { memo, useCallback } from 'react';

const FormInput = memo(({ 
  label, 
  name, 
  value, 
  error, 
  touched, 
  onChange, 
  onBlur, 
  ...props 
}) => {
  console.log(`Rendering ${name}`);
  
  return (
    <div className={`form-field ${error && touched ? 'error' : ''}`}>
      <label htmlFor={name}>{label}:</label>
      <input
        id={name}
        name={name}
        value={value}
        onChange={onChange}
        onBlur={onBlur}
        {...props}
      />
      {error && touched && <span className="error-message">{error}</span>}
    </div>
  );
});

function OptimizedForm() {
  const [formData, setFormData] = useState({ /* ... */ });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

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

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

  return (
    <form>
      <FormInput
        label="First Name"
        name="firstName"
        value={formData.firstName}
        error={errors.firstName}
        touched={touched.firstName}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {/* More optimized inputs... */}
    </form>
  );
}

Comparison Summary

Feature Controlled Components React Hook Form Formik
Performance Good with optimization Excellent Good
Bundle Size 0kB ~9kB ~13kB
Learning Curve Low Medium Medium
Validation Manual/Yup Built-in/Yup Yup-based
TypeScript Excellent Excellent Good
Dynamic Fields Manual useFieldArray Manual

Choose based on your needs:

  • Controlled Components: Simple forms, learning purposes

  • React Hook Form: Complex forms, performance critical

  • Formik: Familiar API, Redux-like pattern preference

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?