Forms and User Input Handling

1. Controlled Components and Input State Management

Input Type Pattern State Binding Event Handler
Text Input <input value={state} onChange={handler} /> State holds current value e.target.value
Textarea <textarea value={state} onChange={handler} /> Same as text input e.target.value
Checkbox <input type="checkbox" checked={state} /> Boolean state e.target.checked
Radio <input type="radio" checked={state === value} /> Compare state to value e.target.value
Select <select value={state} onChange={handler} /> Selected option value e.target.value
Multiple Select <select multiple value={array} /> Array of values Array.from(e.target.selectedOptions)

Example: Controlled component patterns

// Basic text input
const TextInput = () => {
  const [value, setValue] = useState('');
  
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Enter text"
    />
  );
};

// Multiple inputs with single state object
const Form = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: ''
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="age"
        type="number"
        value={formData.age}
        onChange={handleChange}
        placeholder="Age"
      />
    </form>
  );
};

// Checkbox example
const CheckboxInput = () => {
  const [accepted, setAccepted] = useState(false);
  
  return (
    <label>
      <input
        type="checkbox"
        checked={accepted}
        onChange={(e) => setAccepted(e.target.checked)}
      />
      I accept the terms
    </label>
  );
};

// Multiple checkboxes (array)
const MultiCheckbox = () => {
  const [interests, setInterests] = useState([]);
  
  const handleToggle = (interest) => {
    setInterests(prev =>
      prev.includes(interest)
        ? prev.filter(i => i !== interest)
        : [...prev, interest]
    );
  };
  
  return (
    <div>
      {['React', 'Vue', 'Angular'].map(tech => (
        <label key={tech}>
          <input
            type="checkbox"
            checked={interests.includes(tech)}
            onChange={() => handleToggle(tech)}
          />
          {tech}
        </label>
      ))}
    </div>
  );
};

// Radio buttons
const RadioInput = () => {
  const [plan, setPlan] = useState('basic');
  
  return (
    <div>
      {['basic', 'pro', 'enterprise'].map(value => (
        <label key={value}>
          <input
            type="radio"
            name="plan"
            value={value}
            checked={plan === value}
            onChange={(e) => setPlan(e.target.value)}
          />
          {value}
        </label>
      ))}
    </div>
  );
};

// Select dropdown
const SelectInput = () => {
  const [country, setCountry] = useState('');
  
  return (
    <select value={country} onChange={(e) => setCountry(e.target.value)}>
      <option value="">Select country</option>
      <option value="us">United States</option>
      <option value="uk">United Kingdom</option>
      <option value="ca">Canada</option>
    </select>
  );
};
Note: Controlled components have React state as the "single source of truth". Every state mutation happens through setState, providing full control and predictability.

2. Uncontrolled Components and useRef

Approach Implementation When to Use Access Pattern
useRef const ref = useRef(null) Direct DOM access needed ref.current.value
defaultValue <input defaultValue="text" /> Initial value only No state synchronization
File Input Always uncontrolled Security reasons ref.current.files
Form Reset formRef.current.reset() Native form methods Reset all fields at once
Integration Third-party libraries Non-React code Direct DOM manipulation

Example: Uncontrolled components with useRef

import { useRef } from 'react';

// Basic uncontrolled input
const UncontrolledInput = () => {
  const inputRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Value:', inputRef.current.value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="Initial value" />
      <button type="submit">Submit</button>
    </form>
  );
};

// File input (always uncontrolled)
const FileInput = () => {
  const fileRef = useRef(null);
  
  const handleUpload = () => {
    const files = fileRef.current.files;
    if (files.length > 0) {
      console.log('Selected file:', files[0].name);
    }
  };
  
  return (
    <div>
      <input type="file" ref={fileRef} />
      <button onClick={handleUpload}>Upload</button>
    </div>
  );
};

// Form with multiple refs
const UncontrolledForm = () => {
  const nameRef = useRef(null);
  const emailRef = useRef(null);
  const formRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value
    };
    
    console.log('Form data:', formData);
    
    // Reset form using native method
    formRef.current.reset();
  };
  
  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input ref={nameRef} name="name" defaultValue="" />
      <input ref={emailRef} name="email" type="email" defaultValue="" />
      <button type="submit">Submit</button>
      <button type="button" onClick={() => formRef.current.reset()}>
        Clear
      </button>
    </form>
  );
};

// Hybrid approach (controlled + ref for advanced features)
const HybridInput = () => {
  const [value, setValue] = useState('');
  const inputRef = useRef(null);
  
  const handleFocus = () => {
    inputRef.current.focus();
  };
  
  const handleSelectAll = () => {
    inputRef.current.select();
  };
  
  return (
    <div>
      <input
        ref={inputRef}
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button onClick={handleFocus}>Focus</button>
      <button onClick={handleSelectAll}>Select All</button>
    </div>
  );
};

// Integration with third-party library
const ThirdPartyInput = () => {
  const containerRef = useRef(null);
  
  useEffect(() => {
    // Initialize third-party library
    const instance = ExternalLibrary.init(containerRef.current, {
      onChange: (value) => {
        console.log('Value changed:', value);
      }
    });
    
    return () => {
      instance.destroy();
    };
  }, []);
  
  return <div ref={containerRef} />;
};
Warning: Prefer controlled components for most use cases. Use uncontrolled only for file inputs, integration with non-React code, or when you need direct DOM manipulation.

3. Form Validation Patterns and Error Handling

Strategy When to Validate Implementation User Experience
On Submit Form submission Validate all fields in submit handler Less intrusive, batch errors
On Blur Field loses focus Validate in onBlur handler Immediate feedback after editing
On Change Every keystroke Validate in onChange handler Real-time feedback (can be annoying)
Debounced After pause in typing useDebounce + validation Balance between immediate and batch
Mixed Different rules per field Combine strategies Optimal UX per field type

Example: Form validation patterns

// Validation on submit
const SubmitValidation = () => {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});
  
  const validate = () => {
    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';
    }
    
    return newErrors;
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    const validationErrors = validate();
    
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    
    // Submit form
    console.log('Valid form data:', formData);
    setErrors({});
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    // Clear error when user starts typing
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

// Validation on blur (after editing)
const BlurValidation = () => {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);
  const [error, setError] = useState('');
  
  const validateEmail = (value) => {
    if (!value) return 'Email is required';
    if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
    return '';
  };
  
  const handleBlur = () => {
    setTouched(true);
    setError(validateEmail(email));
  };
  
  return (
    <div>
      <input
        value={email}
        onChange={(e) => {
          setEmail(e.target.value);
          if (touched) setError(validateEmail(e.target.value));
        }}
        onBlur={handleBlur}
        placeholder="Email"
      />
      {touched && error && <span className="error">{error}</span>}
    </div>
  );
};

// Custom validation hook
const useFormValidation = (initialState, validationRules) => {
  const [values, setValues] = useState(initialState);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  const validate = (fieldName, value) => {
    const rule = validationRules[fieldName];
    if (!rule) return '';
    
    for (const validator of rule) {
      const error = validator(value, values);
      if (error) return error;
    }
    return '';
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
    
    if (touched[name]) {
      const error = validate(name, value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };
  
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    const error = validate(name, values[name]);
    setErrors(prev => ({ ...prev, [name]: error }));
  };
  
  const validateAll = () => {
    const newErrors = {};
    Object.keys(validationRules).forEach(field => {
      const error = validate(field, values[field]);
      if (error) newErrors[field] = error;
    });
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validateAll
  };
};

// Usage
const ValidationForm = () => {
  const { values, errors, touched, handleChange, handleBlur, validateAll } = 
    useFormValidation(
      { email: '', password: '' },
      {
        email: [
          (v) => !v ? 'Required' : '',
          (v) => !/\S+@\S+\.\S+/.test(v) ? 'Invalid email' : ''
        ],
        password: [
          (v) => !v ? 'Required' : '',
          (v) => v.length < 8 ? 'Min 8 characters' : ''
        ]
      }
    );
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateAll()) {
      console.log('Valid:', values);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={values.email}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {touched.email && errors.email && <span>{errors.email}</span>}
      <button type="submit">Submit</button>
    </form>
  );
};

4. Custom Input Components and Hooks

Pattern Purpose Benefits Example
Wrapper Components Consistent styling and behavior Reusability, DRY, branding Custom TextInput with label/error
useInput Hook Extract input state logic Cleaner components, reusable logic Hook returning value, onChange, reset
useForm Hook Manage entire form state Centralized form logic Hook with values, errors, handlers
Compound Components Flexible input structure Composable API, flexibility Input.Label, Input.Field, Input.Error
Controlled Wrappers Abstract third-party inputs Consistent API, testability Wrap date picker with standard props

Example: Custom input components and hooks

// Custom input hook
const useInput = (initialValue = '', validation) => {
  const [value, setValue] = useState(initialValue);
  const [error, setError] = useState('');
  const [touched, setTouched] = useState(false);
  
  const onChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    
    if (touched && validation) {
      setError(validation(newValue));
    }
  };
  
  const onBlur = () => {
    setTouched(true);
    if (validation) {
      setError(validation(value));
    }
  };
  
  const reset = () => {
    setValue(initialValue);
    setError('');
    setTouched(false);
  };
  
  return {
    value,
    error,
    touched,
    onChange,
    onBlur,
    reset,
    bind: { value, onChange, onBlur }
  };
};

// Usage
const LoginForm = () => {
  const email = useInput('', (v) => 
    !v ? 'Required' : !/\S+@\S+/.test(v) ? 'Invalid' : ''
  );
  const password = useInput('', (v) => 
    v.length < 8 ? 'Min 8 chars' : ''
  );
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email.error && !password.error) {
      console.log({ email: email.value, password: password.value });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input {...email.bind} placeholder="Email" />
      {email.touched && email.error && <span>{email.error}</span>}
      
      <input {...password.bind} type="password" placeholder="Password" />
      {password.touched && password.error && <span>{password.error}</span>}
      
      <button type="submit">Login</button>
    </form>
  );
};

// Custom input wrapper component
const TextField = ({ label, error, touched, ...props }) => {
  return (
    <div className="field">
      {label && <label>{label}</label>}
      <input {...props} className={error && touched ? 'error' : ''} />
      {touched && error && (
        <span className="error-message">{error}</span>
      )}
    </div>
  );
};

// Usage
const FormWithTextField = () => {
  const email = useInput('');
  
  return (
    <TextField
      label="Email"
      type="email"
      error={email.error}
      touched={email.touched}
      {...email.bind}
    />
  );
};

// Compound component pattern
const Input = ({ children, error, touched }) => {
  return (
    <div className="input-group">
      {children}
      {touched && error && <Input.Error>{error}</Input.Error>}
    </div>
  );
};

Input.Label = ({ children, htmlFor }) => (
  <label htmlFor={htmlFor} className="input-label">{children}</label>
);

Input.Field = ({ id, ...props }) => (
  <input id={id} className="input-field" {...props} />
);

Input.Error = ({ children }) => (
  <span className="input-error">{children}</span>
);

// Usage
const CompoundForm = () => {
  const email = useInput('');
  
  return (
    <Input error={email.error} touched={email.touched}>
      <Input.Label htmlFor="email">Email</Input.Label>
      <Input.Field id="email" type="email" {...email.bind} />
    </Input>
  );
};

// Custom form hook
const useForm = (initialValues, validationSchema) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const validateField = (name, value) => {
    if (validationSchema[name]) {
      return validationSchema[name](value, values);
    }
    return '';
  };
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
    
    if (touched[name]) {
      const error = validateField(name, value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };
  
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    const error = validateField(name, values[name]);
    setErrors(prev => ({ ...prev, [name]: error }));
  };
  
  const handleSubmit = async (onSubmit) => {
    return async (e) => {
      e.preventDefault();
      
      // Validate all fields
      const newErrors = {};
      Object.keys(validationSchema).forEach(field => {
        const error = validateField(field, values[field]);
        if (error) newErrors[field] = error;
      });
      
      setErrors(newErrors);
      setTouched(Object.keys(validationSchema).reduce(
        (acc, key) => ({ ...acc, [key]: true }), {}
      ));
      
      if (Object.keys(newErrors).length === 0) {
        setIsSubmitting(true);
        try {
          await onSubmit(values);
        } finally {
          setIsSubmitting(false);
        }
      }
    };
  };
  
  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };
  
  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  };
};

5. Form Libraries Integration (Formik, React Hook Form)

Library Philosophy Pros Cons
Formik Component-based, render props Easy to learn, good DX, field-level validation More re-renders, larger bundle
React Hook Form Hook-based, uncontrolled inputs High performance, small bundle, less re-renders Steeper learning curve, ref-based
Final Form Framework-agnostic core Flexible, powerful, subscription-based More complex API
Yup / Zod Schema validation libraries Type-safe, reusable schemas, great DX Additional dependency

Example: Formik and React Hook Form

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

const FormikForm = () => {
  const formik = useFormik({
    initialValues: {
      email: '',
      password: ''
    },
    validationSchema: Yup.object({
      email: Yup.string()
        .email('Invalid email')
        .required('Required'),
      password: Yup.string()
        .min(8, 'Must be 8 characters or more')
        .required('Required')
    }),
    onSubmit: async (values) => {
      await api.login(values);
    }
  });
  
  return (
    <form onSubmit={formik.handleSubmit}>
      <input
        name="email"
        type="email"
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.email}
      />
      {formik.touched.email && formik.errors.email && (
        <div>{formik.errors.email}</div>
      )}
      
      <input
        name="password"
        type="password"
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
        value={formik.values.password}
      />
      {formik.touched.password && formik.errors.password && (
        <div>{formik.errors.password}</div>
      )}
      
      <button type="submit" disabled={formik.isSubmitting}>
        Submit
      </button>
    </form>
  );
};

// Formik with Formik components
import { Formik, Form, Field, ErrorMessage } from 'formik';

const FormikComponents = () => (
  <Formik
    initialValues={{ email: '', password: '' }}
    validationSchema={validationSchema}
    onSubmit={async (values) => {
      await api.login(values);
    }}
  >
    {({ isSubmitting }) => (
      <Form>
        <Field name="email" type="email" />
        <ErrorMessage name="email" component="div" />
        
        <Field name="password" type="password" />
        <ErrorMessage name="password" component="div" />
        
        <button type="submit" disabled={isSubmitting}>
          Submit
        </button>
      </Form>
    )}
  </Formik>
);

// REACT HOOK FORM Example
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email').min(1, 'Required'),
  password: z.string().min(8, 'Must be at least 8 characters')
});

const ReactHookForm = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm({
    resolver: zodResolver(schema)
  });
  
  const onSubmit = async (data) => {
    await api.login(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email')}
        type="email"
        placeholder="Email"
      />
      {errors.email && <span>{errors.email.message}</span>}
      
      <input
        {...register('password')}
        type="password"
        placeholder="Password"
      />
      {errors.password && <span>{errors.password.message}</span>}
      
      <button type="submit" disabled={isSubmitting}>
        Submit
      </button>
    </form>
  );
};

// React Hook Form with Controller (for custom components)
import { Controller } from 'react-hook-form';

const RHFWithController = () => {
  const { control, handleSubmit } = useForm();
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="customInput"
        control={control}
        rules={{ required: 'This field is required' }}
        render={({ field, fieldState }) => (
          <div>
            <CustomInput {...field} />
            {fieldState.error && <span>{fieldState.error.message}</span>}
          </div>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  );
};
Note: React Hook Form is generally recommended for performance-critical apps due to fewer re-renders. Formik offers easier learning curve and better integration with controlled components.

6. File Upload and FormData Handling

Feature Implementation Key Points Browser API
File Input <input type="file" ref={fileRef} /> Always uncontrolled Access via ref.current.files
Multiple Files <input type="file" multiple /> files is FileList (array-like) Array.from(files)
File Preview URL.createObjectURL(file) Create temporary URL Revoke with URL.revokeObjectURL
FormData new FormData() Build multipart/form-data Append files and fields
Drag and Drop onDrop, onDragOver handlers e.dataTransfer.files preventDefault() required
Progress XMLHttpRequest or axios onUploadProgress callback Track upload percentage

Example: File upload and FormData

// Basic file upload
const FileUpload = () => {
  const [file, setFile] = useState(null);
  const [preview, setPreview] = useState(null);
  
  const handleFileChange = (e) => {
    const selectedFile = e.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      
      // Create preview URL for images
      if (selectedFile.type.startsWith('image/')) {
        const url = URL.createObjectURL(selectedFile);
        setPreview(url);
      }
    }
  };
  
  const handleUpload = async () => {
    if (!file) return;
    
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', 'My file');
    
    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });
      const data = await response.json();
      console.log('Upload success:', data);
    } catch (error) {
      console.error('Upload failed:', error);
    }
  };
  
  // Cleanup preview URL
  useEffect(() => {
    return () => {
      if (preview) URL.revokeObjectURL(preview);
    };
  }, [preview]);
  
  return (
    <div>
      <input type="file" onChange={handleFileChange} accept="image/*" />
      {preview && <img src={preview} alt="Preview" style={{ width: 200 }} />}
      {file && (
        <div>
          <p>File: {file.name}</p>
          <p>Size: {(file.size / 1024).toFixed(2)} KB</p>
          <button onClick={handleUpload}>Upload</button>
        </div>
      )}
    </div>
  );
};

// Multiple file upload
const MultiFileUpload = () => {
  const [files, setFiles] = useState([]);
  
  const handleFileChange = (e) => {
    const fileList = Array.from(e.target.files);
    setFiles(fileList);
  };
  
  const handleUpload = async () => {
    const formData = new FormData();
    files.forEach((file, index) => {
      formData.append(\`files[\${index}]\`, file);
    });
    
    await fetch('/api/upload-multiple', {
      method: 'POST',
      body: formData
    });
  };
  
  return (
    <div>
      <input type="file" multiple onChange={handleFileChange} />
      <ul>
        {files.map((file, index) => (
          <li key={index}>{file.name} - {file.size} bytes</li>
        ))}
      </ul>
      {files.length > 0 && (
        <button onClick={handleUpload}>Upload All</button>
      )}
    </div>
  );
};

// Drag and drop file upload
const DragDropUpload = () => {
  const [files, setFiles] = useState([]);
  const [isDragging, setIsDragging] = useState(false);
  
  const handleDragOver = (e) => {
    e.preventDefault();
    setIsDragging(true);
  };
  
  const handleDragLeave = () => {
    setIsDragging(false);
  };
  
  const handleDrop = (e) => {
    e.preventDefault();
    setIsDragging(false);
    
    const droppedFiles = Array.from(e.dataTransfer.files);
    setFiles(prev => [...prev, ...droppedFiles]);
  };
  
  return (
    <div
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
      style={{
        border: isDragging ? '2px solid blue' : '2px dashed gray',
        padding: '2rem',
        textAlign: 'center'
      }}
    >
      {files.length === 0 ? (
        <p>Drag and drop files here</p>
      ) : (
        <ul>
          {files.map((file, i) => (
            <li key={i}>{file.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
};

// Upload with progress
const UploadWithProgress = () => {
  const [file, setFile] = useState(null);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  
  const handleUpload = async () => {
    if (!file) return;
    
    setUploading(true);
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      // Using axios for upload progress
      const response = await axios.post('/api/upload', formData, {
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        }
      });
      console.log('Upload complete:', response.data);
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };
  
  return (
    <div>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files[0])}
        disabled={uploading}
      />
      {uploading && (
        <div>
          <progress value={progress} max="100" />
          <span>{progress}%</span>
        </div>
      )}
      {file && !uploading && (
        <button onClick={handleUpload}>Upload</button>
      )}
    </div>
  );
};

// Complete form with file and other fields
const CompleteForm = () => {
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    file: null
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  const handleFileChange = (e) => {
    setFormData(prev => ({ ...prev, file: e.target.files[0] }));
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const data = new FormData();
    data.append('title', formData.title);
    data.append('description', formData.description);
    if (formData.file) {
      data.append('file', formData.file);
    }
    
    await fetch('/api/submit', {
      method: 'POST',
      body: data
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="title"
        value={formData.title}
        onChange={handleChange}
        placeholder="Title"
      />
      <textarea
        name="description"
        value={formData.description}
        onChange={handleChange}
        placeholder="Description"
      />
      <input type="file" onChange={handleFileChange} />
      <button type="submit">Submit</button>
    </form>
  );
};
Warning: Always validate file types and sizes on the server. Client-side validation (accept attribute) is only for UX. Remember to revoke object URLs with URL.revokeObjectURL() to prevent memory leaks.

Forms Best Practices

  • Controlled components: Prefer for most inputs - React state as single source of truth
  • Validation timing: Validate on blur for better UX, on submit as final check
  • Form libraries: Use React Hook Form for performance, Formik for easier DX
  • Custom hooks: Extract form logic for reusability and cleaner components
  • File uploads: Always uncontrolled, use FormData, validate server-side
  • Error messages: Show after touched, clear on change, batch on submit