Skip to main content

Form Handling Approaches

React provides multiple ways to handle forms and user input. Understanding when to use each approach is key to building efficient forms.

Using Refs

Refs provide direct access to DOM elements, useful for reading values on submission:
import { useRef } from 'react';

export default function Login() {
  const email = useRef();
  const password = useRef();

  function handleSubmit(event) {
    event.preventDefault();

    const enteredEmail = email.current.value;
    const enteredPassword = password.current.value;

    console.log(enteredEmail, enteredPassword);
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>

      <div className="control-row">
        <div className="control no-margin">
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" ref={email} />
        </div>

        <div className="control no-margin">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" name="password" ref={password} />
        </div>
      </div>

      <p className="form-actions">
        <button className="button button-flat">Reset</button>
        <button className="button">Login</button>
      </p>
    </form>
  );
}
Refs are ideal when you only need the value on submission and don’t need to validate on every keystroke.

Using FormData API

The native FormData API provides an elegant way to extract all form values at once:
function handleSubmit(event) {
  event.preventDefault();

  const formData = new FormData(event.target);
  const data = Object.fromEntries(formData.entries());
  
  // Get multiple values for checkboxes/multi-select
  const acquisitionChannel = formData.getAll('acquisition');
  
  console.log(data);
  
  // Reset the form
  event.target.reset();
}

return (
  <form onSubmit={handleSubmit}>
    <input name="email" type="email" />
    <input name="password" type="password" />
    <button>Submit</button>
  </form>
);
Make sure each input has a name attribute for FormData to work correctly.

Input Validation

Validation on Keystroke

Validate input as the user types for immediate feedback:
import { useState } from 'react';

function EmailInput() {
  const [email, setEmail] = useState('');
  const emailIsInvalid = email !== '' && !email.includes('@');

  return (
    <div className="control">
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {emailIsInvalid && (
        <p className="error">Please enter a valid email address.</p>
      )}
    </div>
  );
}

Validation on Blur

Show errors only after the user leaves an input field:
import { useState } from 'react';

function EmailInput() {
  const [enteredEmail, setEnteredEmail] = useState('');
  const [didEdit, setDidEdit] = useState(false);

  const emailIsInvalid = didEdit && !enteredEmail.includes('@');

  function handleInputChange(event) {
    setEnteredEmail(event.target.value);
    setDidEdit(false);
  }

  function handleInputBlur() {
    setDidEdit(true);
  }

  return (
    <div className="control">
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={enteredEmail}
        onChange={handleInputChange}
        onBlur={handleInputBlur}
      />
      {emailIsInvalid && (
        <p className="error">Please enter a valid email.</p>
      )}
    </div>
  );
}
Validating on every keystroke can be annoying for users. Consider using blur or submission validation for better UX.

Custom Input Hook

Extract input logic into a reusable hook:
import { useState } from 'react';

export function useInput(defaultValue, validationFn) {
  const [enteredValue, setEnteredValue] = useState(defaultValue);
  const [didEdit, setDidEdit] = useState(false);

  const valueIsValid = validationFn(enteredValue);

  function handleInputChange(event) {
    setEnteredValue(event.target.value);
    setDidEdit(false);
  }

  function handleInputBlur() {
    setDidEdit(true);
  }

  return {
    value: enteredValue,
    handleInputChange,
    handleInputBlur,
    hasError: didEdit && !valueIsValid
  };
}

Using the Custom Hook

import { useInput } from '../hooks/useInput';
import { isEmail, hasMinLength } from '../util/validation';

function Login() {
  const {
    value: emailValue,
    handleInputChange: handleEmailChange,
    handleInputBlur: handleEmailBlur,
    hasError: emailHasError
  } = useInput('', isEmail);

  const {
    value: passwordValue,
    handleInputChange: handlePasswordChange,
    handleInputBlur: handlePasswordBlur,
    hasError: passwordHasError
  } = useInput('', (value) => hasMinLength(value, 6));

  function handleSubmit(event) {
    event.preventDefault();

    if (emailHasError || passwordHasError) {
      return;
    }

    console.log('Submitting:', { emailValue, passwordValue });
  }

  return (
    <form onSubmit={handleSubmit}>
      <div className="control">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={emailValue}
          onChange={handleEmailChange}
          onBlur={handleEmailBlur}
        />
        {emailHasError && <p className="error">Invalid email.</p>}
      </div>

      <div className="control">
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={passwordValue}
          onChange={handlePasswordChange}
          onBlur={handlePasswordBlur}
        />
        {passwordHasError && <p className="error">Password too short.</p>}
      </div>

      <button>Login</button>
    </form>
  );
}

Validation Helper Functions

Create reusable validation utilities:
export function isEmail(value) {
  return value.includes('@');
}

export function isNotEmpty(value) {
  return value.trim() !== '';
}

export function hasMinLength(value, minLength) {
  return value.length >= minLength;
}

export function isEqualToOtherValue(value, otherValue) {
  return value === otherValue;
}

Form Handling Comparison

Refs

Best for: Simple forms, read-on-submitPros: Minimal re-renders, simple codeCons: No real-time validation

State

Best for: Complex validation, controlled inputsPros: Full control, real-time validationCons: More re-renders, more code

FormData

Best for: Standard forms, native validationPros: Clean API, built-in browser supportCons: Less control over validation timing
Source Code: Section 17 - Forms & User Input

Build docs developers (and LLMs) love