Intermediate90 min read

Handling Forms and User Input

Learn how to handle forms, controlled components, validation, and user interactions in React.

Topics Covered:

Controlled ComponentsForm HandlingValidationuseActionStateuseFormStatusServer Actions

Prerequisites:

  • Managing State with useState

Video Tutorial

Overview

Forms are central to most web applications. This tutorial covers both traditional form handling with controlled components and modern React 19 patterns using useActionState and useFormStatus. You'll learn how to create forms with validation, handle submissions, manage form state, and leverage React 19's powerful form features for better developer experience and user experience.

Lesson 1: Understanding Controlled Components

Controlled components are the foundation of React form handling. Understanding them is essential before moving to React 19 patterns. What are Controlled Components? • Input value is controlled by React state • onChange handler updates state • State is the single source of truth • Enables validation and transformation Benefits: • Predictable form state • Easy validation • Full control over input values • Can transform values before setting state vs Uncontrolled Components: • Controlled: Value in state, onChange updates state • Uncontrolled: Value in DOM, use ref to access

Code Example:
// Controlled component
function ControlledInput() {
  const [value, setValue] = useState('');
  
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

// Uncontrolled component
function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  const handleSubmit = () => {
    const value = inputRef.current?.value;
    console.log(value);
  };
  
  return (
    <input
      ref={inputRef}
      defaultValue="initial"
    />
  );
}

// Controlled form example
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log({ email, password });
    // Submit form
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

// Why controlled?
// ✅ Can validate in real-time
// ✅ Can transform values (uppercase, format)
// ✅ Can disable submit button based on state
// ✅ Can reset form easily
// ✅ Can access values without refs

Controlled components give you full control over form state. The value comes from state, and onChange updates state. This enables validation, transformation, and better form management.

Lesson 2: Form Validation Patterns

Validation ensures users enter correct data. Learn different validation approaches. Validation Types: • Client-side validation (immediate feedback) • Server-side validation (security) • Real-time validation (as user types) • On-submit validation (before sending) Best Practices: • Validate on both client and server • Show clear error messages • Don't block user input • Validate on blur for better UX • Use HTML5 validation attributes

Code Example:
// Real-time validation
function EmailInput() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    
    // Validate as user types
    if (value && !value.includes('@')) {
      setError('Please enter a valid email');
    } else {
      setError('');
    }
  };
  
  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        className={error ? 'error' : ''}
      />
      {error && <span className="error-text">{error}</span>}
    </div>
  );
}

// Validation on blur (better UX)
function EmailInputBlur() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  const [touched, setTouched] = useState(false);
  
  const validate = (value: string) => {
    if (!value) return 'Email is required';
    if (!value.includes('@')) return 'Invalid email format';
    return '';
  };
  
  const handleBlur = () => {
    setTouched(true);
    setError(validate(email));
  };
  
  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={handleBlur}
        className={touched && error ? 'error' : ''}
      />
      {touched && error && (
        <span className="error-text">{error}</span>
      )}
    </div>
  );
}

// Multiple field validation
function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  const validate = (name: string, value: string) => {
    switch (name) {
      case 'email':
        if (!value.includes('@')) return 'Invalid email';
        break;
      case 'password':
        if (value.length < 8) return 'Password must be 8+ characters';
        break;
      case 'name':
        if (value.length < 2) return 'Name must be 2+ characters';
        break;
    }
    return '';
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // Validate field
    const error = validate(name, value);
    setErrors(prev => ({ ...prev, [name]: error }));
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // Validate all fields
    const newErrors: Record<string, string> = {};
    Object.entries(formData).forEach(([key, value]) => {
      const error = validate(key, value);
      if (error) newErrors[key] = error;
    });
    
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }
    
    // Submit form
    console.log('Form valid:', formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        className={errors.name ? 'error' : ''}
      />
      {errors.name && <span>{errors.name}</span>}
      
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        className={errors.email ? 'error' : ''}
      />
      {errors.email && <span>{errors.email}</span>}
      
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        className={errors.password ? 'error' : ''}
      />
      {errors.password && <span>{errors.password}</span>}
      
      <button type="submit">Register</button>
    </form>
  );
}

// HTML5 validation
function HTML5Form() {
  return (
    <form>
      <input
        type="email"
        required
        pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
        title="Enter a valid email address"
      />
      <input
        type="password"
        required
        minLength={8}
        title="Password must be at least 8 characters"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Validation can happen in real-time, on blur, or on submit. Combine client-side validation with server-side validation for security. Show clear error messages and don't block user input.

Lesson 3: React 19 - Server Actions and useActionState

React 19 introduces useActionState (formerly useFormState) for managing forms with server actions. This is the modern way to handle forms in React 19 and Next.js. What are Server Actions? • Async functions that run on the server • Called directly from client components • Type-safe end-to-end • No API routes needed useActionState Benefits: • Built-in pending state • Automatic form state management • Works with Server Actions • Better TypeScript support • Simplified form handling When to Use: • Forms with server-side processing • Next.js App Router applications • When you want built-in pending state • Type-safe form handling

Code Example:
// Step 1: Create Server Action
// app/actions.ts
'use server';

export interface FormState {
  message?: string;
  error?: string;
  success?: boolean;
}

export async function submitContactForm(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;
  
  // Validate
  if (!email || !email.includes('@')) {
    return {
      error: 'Please enter a valid email address',
      success: false,
    };
  }
  
  if (!message || message.trim().length < 10) {
    return {
      error: 'Message must be at least 10 characters',
      success: false,
    };
  }
  
  // Process (save to database, send email, etc.)
  try {
    await saveContactForm({ email, message });
    return {
      message: 'Thank you for your message!',
      success: true,
    };
  } catch (error) {
    return {
      error: 'Failed to send message. Please try again.',
      success: false,
    };
  }
}

// Step 2: Use in Component
'use client';

import { useActionState } from 'react';
import { submitContactForm } from '@/app/actions';

const initialState: FormState = {};

function ContactForm() {
  const [state, formAction, pending] = useActionState(
    submitContactForm,
    initialState
  );
  
  return (
    <form action={formAction}>
      <input
        name="email"
        type="email"
        required
        disabled={pending}
      />
      
      <textarea
        name="message"
        required
        disabled={pending}
        minLength={10}
      />
      
      {state.error && (
        <div className="error">{state.error}</div>
      )}
      
      {state.message && (
        <div className="success">{state.message}</div>
      )}
      
      <button type="submit" disabled={pending}>
        {pending ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

// useActionState returns:
// - state: Current form state (from server action)
// - formAction: Action wrapper for form
// - pending: Boolean indicating if action is in progress

// Field-level errors
interface RegistrationState {
  errors?: {
    email?: string;
    password?: string;
    name?: string;
  };
  message?: string;
}

'use server';
export async function registerUser(
  prevState: RegistrationState,
  formData: FormData
): Promise<RegistrationState> {
  const errors: RegistrationState['errors'] = {};
  
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  const name = formData.get('name') as string;
  
  // Validate each field
  if (!email || !email.includes('@')) {
    errors.email = 'Invalid email address';
  }
  
  if (!password || password.length < 8) {
    errors.password = 'Password must be at least 8 characters';
  }
  
  if (!name || name.length < 2) {
    errors.name = 'Name must be at least 2 characters';
  }
  
  if (Object.keys(errors).length > 0) {
    return { errors };
  }
  
  // Create user
  await createUser({ email, password, name });
  
  return { message: 'Account created successfully!' };
}

// Using field-level errors
function RegistrationForm() {
  const [state, formAction, pending] = useActionState(
    registerUser,
    {}
  );
  
  return (
    <form action={formAction}>
      <div>
        <input name="name" disabled={pending} />
        {state.errors?.name && (
          <span className="error">{state.errors.name}</span>
        )}
      </div>
      
      <div>
        <input name="email" type="email" disabled={pending} />
        {state.errors?.email && (
          <span className="error">{state.errors.email}</span>
        )}
      </div>
      
      <div>
        <input name="password" type="password" disabled={pending} />
        {state.errors?.password && (
          <span className="error">{state.errors.password}</span>
        )}
      </div>
      
      {state.message && (
        <div className="success">{state.message}</div>
      )}
      
      <button type="submit" disabled={pending}>
        {pending ? 'Creating Account...' : 'Register'}
      </button>
    </form>
  );
}

useActionState simplifies form handling with server actions. It provides built-in pending state and automatic form state management. Server actions run on the server, providing security and type safety.

Lesson 4: useFormStatus for Form State Access

useFormStatus lets child components access form submission status without prop drilling. Key Features: • Automatically detects form status • No props needed • Works with Server Actions • Must be used inside <form> element Benefits: • Eliminates prop drilling • Better component composition • Cleaner code • Automatic status detection When to Use: • Submit buttons • Form inputs that need to disable during submission • Loading indicators • Form validation messages

Code Example:
import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';

// Server action
'use server';
export async function submitForm(
  prevState: any,
  formData: FormData
) {
  await new Promise(resolve => setTimeout(resolve, 2000));
  return { message: 'Submitted!' };
}

// Form component
function ContactForm() {
  const [state, formAction] = useActionState(submitForm, {});
  
  return (
    <form action={formAction}>
      <FormInput name="email" type="email" />
      <FormInput name="message" type="textarea" />
      
      {/* Child components automatically know form status */}
      <SubmitButton>Send Message</SubmitButton>
      
      {state.message && <SuccessMessage>{state.message}</SuccessMessage>}
    </form>
  );
}

// SubmitButton uses useFormStatus (no props needed!)
function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? (
        <>
          <Spinner />
          Submitting...
        </>
      ) : (
        children
      )}
    </button>
  );
}

// FormInput also uses useFormStatus
function FormInput({ 
  name, 
  type = 'text',
  ...props 
}: { 
  name: string;
  type?: string;
}) {
  const { pending } = useFormStatus();
  
  if (type === 'textarea') {
    return (
      <textarea
        name={name}
        disabled={pending}
        {...props}
      />
    );
  }
  
  return (
    <input
      name={name}
      type={type}
      disabled={pending}
      {...props}
    />
  );
}

// Form-wide loading overlay
function FormOverlay() {
  const { pending } = useFormStatus();
  
  if (!pending) return null;
  
  return (
    <div className="form-overlay">
      <Spinner />
      <p>Submitting form...</p>
    </div>
  );
}

// Complete form example
function CompleteForm() {
  const [state, formAction] = useActionState(submitForm, {});
  
  return (
    <form action={formAction}>
      <FormOverlay />
      
      <FormInput name="name" required />
      <FormInput name="email" type="email" required />
      <FormInput name="message" type="textarea" required />
      
      <SubmitButton>Submit</SubmitButton>
      
      {state.error && <ErrorMessage>{state.error}</ErrorMessage>}
      {state.message && <SuccessMessage>{state.message}</SuccessMessage>}
    </form>
  );
}

// useFormStatus provides:
// - pending: Boolean - true when form is submitting
// - data: FormData - current form data
// - method: 'get' | 'post' - HTTP method
// - action: string | Function - form action

// Accessing form data
function FormDataPreview() {
  const { pending, data } = useFormStatus();
  
  if (!pending || !data) return null;
  
  return (
    <div className="form-preview">
      <p>Submitting:</p>
      <ul>
        {Array.from(data.entries()).map(([key, value]) => (
          <li key={key}>
            {key}: {String(value)}
          </li>
        ))}
      </ul>
    </div>
  );
}

useFormStatus eliminates prop drilling by automatically detecting form submission status. Child components can access pending state, form data, and other form information without receiving props.

Lesson 5: Combining Traditional and React 19 Patterns

You can combine traditional controlled components with React 19 hooks for maximum flexibility. Hybrid Approach: • Use controlled components for complex validation • Use useActionState for submission • Use useFormStatus for child components • Combine client and server validation Best Practices: • Validate on client for UX • Validate on server for security • Use controlled components for complex forms • Use useActionState for simple forms • Leverage useFormStatus to avoid prop drilling

Code Example:
// Hybrid: Controlled inputs + useActionState
function AdvancedForm() {
  // Traditional controlled state for complex validation
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [clientErrors, setClientErrors] = useState<Record<string, string>>({});
  
  // React 19 for submission
  const [serverState, formAction, pending] = useActionState(
    submitForm,
    {}
  );
  
  // Client-side validation
  const validateField = (name: string, value: string) => {
    const errors: Record<string, string> = {};
    
    if (name === 'email' && value && !value.includes('@')) {
      errors.email = 'Invalid email';
    }
    
    if (name === 'password' && value && value.length < 8) {
      errors.password = 'Password too short';
    }
    
    setClientErrors(prev => ({ ...prev, ...errors }));
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    validateField(name, value);
  };
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Client-side validation before submit
    if (Object.keys(clientErrors).length > 0) {
      return;
    }
    
    // Convert to FormData for server action
    const formDataObj = new FormData();
    formDataObj.append('email', formData.email);
    formDataObj.append('password', formData.password);
    
    // Submit with server action
    await formAction(formDataObj);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        disabled={pending}
      />
      {clientErrors.email && (
        <span className="error">{clientErrors.email}</span>
      )}
      {serverState.errors?.email && (
        <span className="error">{serverState.errors.email}</span>
      )}
      
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        disabled={pending}
      />
      {clientErrors.password && (
        <span className="error">{clientErrors.password}</span>
      )}
      
      <SubmitButton />
    </form>
  );
}

// SubmitButton uses useFormStatus
function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

// Progressive enhancement pattern
function ProgressiveForm() {
  // Works without JavaScript (HTML5 validation)
  // Enhanced with React for better UX
  const [state, formAction, pending] = useActionState(submitForm, {});
  
  return (
    <form action={formAction}>
      <input
        name="email"
        type="email"
        required
        pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
        disabled={pending}
      />
      
      <SubmitButton />
      
      {/* Enhanced with React */}
      {state.error && <ErrorMessage>{state.error}</ErrorMessage>}
      {state.message && <SuccessMessage>{state.message}</SuccessMessage>}
    </form>
  );
}

Combine traditional controlled components with React 19 hooks for maximum flexibility. Use controlled components for complex client-side validation and useActionState for server-side processing.

Lesson 6: Form State Management Patterns

Learn different patterns for managing form state effectively. State Management Options: • Individual useState for each field • Single useState object for all fields • useReducer for complex forms • useActionState for server actions • Custom hooks for reusable logic Choosing the Right Pattern: • Simple forms: Individual useState or object • Complex forms: useReducer • Server actions: useActionState • Reusable logic: Custom hooks

Code Example:
// Pattern 1: Individual useState (simple forms)
function SimpleForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  
  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

// Pattern 2: Single state object (better for multiple fields)
function ObjectForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
    </form>
  );
}

// Pattern 3: useReducer (complex forms)
type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string }
  | { type: 'SET_ERRORS'; errors: Record<string, string> }
  | { type: 'RESET' };

function reducer(state: any, action: FormAction) {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

function ComplexForm() {
  const [state, dispatch] = useReducer(reducer, {
    name: '',
    email: '',
    errors: {},
  });
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: 'SET_FIELD',
      field: e.target.name,
      value: e.target.value,
    });
  };
  
  return (
    <form>
      <input
        name="name"
        value={state.name}
        onChange={handleChange}
      />
    </form>
  );
}

// Pattern 4: Custom hook for reusable form logic
function useForm<T extends Record<string, any>>(initialValues: T) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  
  const setValue = (name: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [name]: value }));
    // Clear error when field changes
    if (errors[name]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  };
  
  const setError = (name: keyof T, error: string) => {
    setErrors(prev => ({ ...prev, [name]: error }));
  };
  
  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };
  
  return {
    values,
    errors,
    setValue,
    setError,
    reset,
  };
}

// Using custom hook
function FormWithHook() {
  const { values, errors, setValue, setError } = useForm({
    email: '',
    password: '',
  });
  
  return (
    <form>
      <input
        value={values.email}
        onChange={e => setValue('email', e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
    </form>
  );
}

// Pattern 5: React 19 with useActionState
function ModernForm() {
  const [state, formAction, pending] = useActionState(submitForm, {});
  
  // Simpler - state managed by useActionState
  return (
    <form action={formAction}>
      <input name="email" disabled={pending} />
      {state.errors?.email && <span>{state.errors.email}</span>}
      <button type="submit" disabled={pending}>Submit</button>
    </form>
  );
}

Choose the right state management pattern based on form complexity. Simple forms can use individual useState, complex forms benefit from useReducer, and React 19 forms can use useActionState for built-in state management.

Lesson 7: Real-World Form Examples

See complete, production-ready form examples using React 19 patterns. Example Forms: • Contact form • Registration form • Login form • Search form • Multi-step form Each example demonstrates: • Proper validation • Error handling • Loading states • Success feedback • Accessibility

Code Example:
// Example 1: Contact Form with React 19
'use server';
export async function submitContact(
  prevState: any,
  formData: FormData
) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;
  
  if (!name || name.trim().length < 2) {
    return { error: 'Name must be at least 2 characters' };
  }
  
  if (!email || !email.includes('@')) {
    return { error: 'Invalid email address' };
  }
  
  if (!message || message.trim().length < 10) {
    return { error: 'Message must be at least 10 characters' };
  }
  
  await sendEmail({ name, email, message });
  
  return { message: 'Message sent successfully!' };
}

function ContactForm() {
  const [state, formAction, pending] = useActionState(submitContact, {});
  
  return (
    <form action={formAction} className="contact-form">
      <FormInput
        name="name"
        label="Name"
        required
        minLength={2}
      />
      
      <FormInput
        name="email"
        type="email"
        label="Email"
        required
      />
      
      <FormTextarea
        name="message"
        label="Message"
        required
        minLength={10}
      />
      
      {state.error && (
        <ErrorMessage>{state.error}</ErrorMessage>
      )}
      
      {state.message && (
        <SuccessMessage>{state.message}</SuccessMessage>
      )}
      
      <SubmitButton>Send Message</SubmitButton>
    </form>
  );
}

// Reusable form components
function FormInput({ 
  name, 
  label, 
  type = 'text',
  ...props 
}: {
  name: string;
  label: string;
  type?: string;
}) {
  const { pending } = useFormStatus();
  
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        name={name}
        type={type}
        disabled={pending}
        {...props}
      />
    </div>
  );
}

function FormTextarea({ 
  name, 
  label,
  ...props 
}: {
  name: string;
  label: string;
}) {
  const { pending } = useFormStatus();
  
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <textarea
        id={name}
        name={name}
        disabled={pending}
        {...props}
      />
    </div>
  );
}

function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? (
        <>
          <Spinner />
          Submitting...
        </>
      ) : (
        children
      )}
    </button>
  );
}

// Example 2: Multi-step form
function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    personal: { name: '', email: '' },
    address: { street: '', city: '' },
    payment: { card: '', cvv: '' },
  });
  
  const [state, formAction, pending] = useActionState(submitForm, {});
  
  const handleNext = () => {
    setStep(s => s + 1);
  };
  
  const handleBack = () => {
    setStep(s => s - 1);
  };
  
  return (
    <form action={formAction}>
      {step === 1 && (
        <PersonalInfoStep
          data={formData.personal}
          onChange={(data) => setFormData(prev => ({
            ...prev,
            personal: data,
          }))}
        />
      )}
      
      {step === 2 && (
        <AddressStep
          data={formData.address}
          onChange={(data) => setFormData(prev => ({
            ...prev,
            address: data,
          }))}
        />
      )}
      
      {step === 3 && (
        <PaymentStep
          data={formData.payment}
          onChange={(data) => setFormData(prev => ({
            ...prev,
            payment: data,
          }))}
        />
      )}
      
      <div className="form-actions">
        {step > 1 && (
          <button type="button" onClick={handleBack} disabled={pending}>
            Back
          </button>
        )}
        {step < 3 ? (
          <button type="button" onClick={handleNext}>
            Next
          </button>
        ) : (
          <SubmitButton>Submit</SubmitButton>
        )}
      </div>
    </form>
  );
}

// Example 3: Search form with debouncing
function SearchForm() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const debouncedQuery = useDebounce(query, 300);
  
  useEffect(() => {
    if (debouncedQuery) {
      search(debouncedQuery).then(setResults);
    }
  }, [debouncedQuery]);
  
  return (
    <form>
      <input
        type="search"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {results.length > 0 && (
        <ul>
          {results.map(result => (
            <li key={result.id}>{result.title}</li>
          ))}
        </ul>
      )}
    </form>
  );
}

Real-world forms combine validation, error handling, loading states, and user feedback. Use React 19 hooks for simpler, more maintainable form code. Extract reusable form components for consistency.

Lesson 8: Accessibility and Best Practices

Accessible forms are essential for all users. Learn best practices. Accessibility Requirements: • Proper labels for all inputs • Error messages associated with fields • Keyboard navigation • Screen reader support • Focus management • ARIA attributes Best Practices: • Use semantic HTML • Associate labels with inputs • Provide clear error messages • Use proper input types • Support keyboard navigation • Test with screen readers

Code Example:
// Accessible form example
function AccessibleForm() {
  const [state, formAction, pending] = useActionState(submitForm, {});
  
  return (
    <form action={formAction} aria-label="Contact form">
      <div>
        <label htmlFor="name">
          Name <span aria-label="required">*</span>
        </label>
        <input
          id="name"
          name="name"
          type="text"
          required
          aria-required="true"
          aria-invalid={!!state.errors?.name}
          aria-describedby={state.errors?.name ? 'name-error' : undefined}
          disabled={pending}
        />
        {state.errors?.name && (
          <span id="name-error" role="alert" className="error">
            {state.errors.name}
          </span>
        )}
      </div>
      
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          required
          aria-required="true"
          aria-invalid={!!state.errors?.email}
          aria-describedby={state.errors?.email ? 'email-error' : undefined}
          disabled={pending}
        />
        {state.errors?.email && (
          <span id="email-error" role="alert" className="error">
            {state.errors.email}
          </span>
        )}
      </div>
      
      <button
        type="submit"
        disabled={pending}
        aria-busy={pending}
      >
        {pending ? 'Submitting...' : 'Submit'}
      </button>
      
      {state.message && (
        <div role="status" aria-live="polite">
          {state.message}
        </div>
      )}
    </form>
  );
}

// Accessible form components
function AccessibleInput({
  id,
  label,
  error,
  ...props
}: {
  id: string;
  label: string;
  error?: string;
}) {
  const { pending } = useFormStatus();
  
  return (
    <div>
      <label htmlFor={id}>
        {label}
        {props.required && <span aria-label="required">*</span>}
      </label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? `${id}-error` : undefined}
        disabled={pending}
        {...props}
      />
      {error && (
        <span
          id={`${id}-error`}
          role="alert"
          className="error"
        >
          {error}
        </span>
      )}
    </div>
  );
}

// Form validation feedback
function FormField({ 
  id, 
  label, 
  error, 
  children 
}: {
  id: string;
  label: string;
  error?: string;
  children: React.ReactNode;
}) {
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      {children}
      {error && (
        <span
          id={`${id}-error`}
          role="alert"
          aria-live="polite"
          className="error"
        >
          {error}
        </span>
      )}
    </div>
  );
}

// Keyboard navigation
function KeyboardAccessibleForm() {
  const formRef = useRef<HTMLFormElement>(null);
  
  const handleKeyDown = (e: React.KeyboardEvent) => {
    // Escape to reset form
    if (e.key === 'Escape') {
      formRef.current?.reset();
    }
  };
  
  return (
    <form
      ref={formRef}
      onKeyDown={handleKeyDown}
      aria-label="Form with keyboard shortcuts"
    >
      {/* Form fields */}
    </form>
  );
}

Accessible forms use proper labels, ARIA attributes, error associations, and keyboard navigation. Always test with screen readers and ensure all users can use your forms effectively.

Conclusion

Form handling in React has evolved significantly with React 19. Use controlled components for complex validation, useActionState for server-side processing, and useFormStatus to eliminate prop drilling. Always validate on both client and server, provide clear feedback, and ensure your forms are accessible to all users.