RE

Forms

React · 6 entries

Controlled Inputs

syntax
<input value={state} onChange={e => setState(e.target.value)} />
example
function EmailInput() {
  const [email, setEmail] = useState('');
  const isValid = email.includes('@') && email.includes('.');

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        className={isValid ? 'valid' : 'invalid'}
      />
      {!isValid && email.length > 0 && (
        <small>Please enter a valid email address</small>
      )}
    </div>
  );
}

Note Controlled inputs keep React as the single source of truth. The value prop locks the input to the state. If you set value without onChange, the input becomes read-only.

Uncontrolled Inputs

syntax
<input defaultValue={initial} ref={inputRef} />
// Read with: inputRef.current.value
example
function QuickSearch() {
  const searchRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    const query = searchRef.current.value;
    performSearch(query);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={searchRef} defaultValue="" placeholder="Search..." />
      <button type="submit">Go</button>
    </form>
  );
}

Note Use defaultValue (not value) for uncontrolled inputs. The DOM holds the state. Uncontrolled inputs are simpler for cases where you only need the value at submission time. For dynamic validation or conditional logic, controlled inputs are better.

Form Submission

syntax
// Traditional
<form onSubmit={handleSubmit}>
// React 19 Actions
<form action={actionFn}>
example
// Traditional approach
function FeedbackForm() {
  const [message, setMessage] = useState('');
  const [submitted, setSubmitted] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    await sendFeedback(message);
    setSubmitted(true);
    setMessage('');
  }

  if (submitted) return <p>Thank you for your feedback!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={message}
        onChange={e => setMessage(e.target.value)}
        required
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Note With onSubmit, always call e.preventDefault() to stop the browser from reloading the page. With React 19 form actions, preventDefault is handled automatically. Both patterns are valid -- actions are newer and reduce boilerplate.

Validation Patterns

syntax
// Validate on change, blur, or submit
const errors = {};
if (!value) errors.field = 'Required';
example
function SignupForm() {
  const [fields, setFields] = useState({ name: '', email: '', age: '' });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  function validate(data) {
    const errs = {};
    if (!data.name.trim()) errs.name = 'Name is required';
    if (!data.email.includes('@')) errs.email = 'Invalid email';
    if (Number(data.age) < 18) errs.age = 'Must be at least 18';
    return errs;
  }

  function handleChange(field, value) {
    const updated = { ...fields, [field]: value };
    setFields(updated);
    if (touched[field]) {
      setErrors(validate(updated));
    }
  }

  function handleBlur(field) {
    setTouched(prev => ({ ...prev, [field]: true }));
    setErrors(validate(fields));
  }

  function handleSubmit(e) {
    e.preventDefault();
    const errs = validate(fields);
    setErrors(errs);
    setTouched({ name: true, email: true, age: true });
    if (Object.keys(errs).length === 0) {
      submitSignup(fields);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={fields.name}
        onChange={e => handleChange('name', e.target.value)}
        onBlur={() => handleBlur('name')}
      />
      {touched.name && errors.name && <span>{errors.name}</span>}
      {/* repeat for other fields */}
      <button type="submit">Sign Up</button>
    </form>
  );
}

Note Validate on blur for a good UX: users see errors after leaving a field, not while typing. On submit, validate everything and mark all fields as touched. For complex forms, consider a form library or useReducer.

Multiple Inputs with One Handler

syntax
function handleChange(e) {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
}
example
function AddressForm() {
  const [address, setAddress] = useState({
    street: '',
    city: '',
    state: '',
    zip: '',
  });

  function handleChange(e) {
    const { name, value } = e.target;
    setAddress(prev => ({ ...prev, [name]: value }));
  }

  return (
    <form>
      <input name="street" value={address.street} onChange={handleChange} placeholder="Street" />
      <input name="city" value={address.city} onChange={handleChange} placeholder="City" />
      <input name="state" value={address.state} onChange={handleChange} placeholder="State" />
      <input name="zip" value={address.zip} onChange={handleChange} placeholder="ZIP" />
    </form>
  );
}

Note Use the name attribute on inputs and a computed property name ([name]) to handle many fields with one function. This scales well and avoids writing separate handlers for each field.

Select, Checkbox & Radio

syntax
// Select
<select value={state} onChange={handler}>...
// Checkbox
<input type="checkbox" checked={state} onChange={handler} />
// Radio
<input type="radio" value="opt" checked={state === 'opt'} onChange={handler} />
example
function PreferencesForm() {
  const [color, setColor] = useState('blue');
  const [newsletter, setNewsletter] = useState(false);
  const [plan, setPlan] = useState('free');

  return (
    <form>
      {/* Select */}
      <label>Favorite Color</label>
      <select value={color} onChange={e => setColor(e.target.value)}>
        <option value="blue">Blue</option>
        <option value="green">Green</option>
        <option value="red">Red</option>
      </select>

      {/* Checkbox */}
      <label>
        <input
          type="checkbox"
          checked={newsletter}
          onChange={e => setNewsletter(e.target.checked)}
        />
        Subscribe to newsletter
      </label>

      {/* Radio */}
      {['free', 'pro', 'enterprise'].map(option => (
        <label key={option}>
          <input
            type="radio"
            name="plan"
            value={option}
            checked={plan === option}
            onChange={e => setPlan(e.target.value)}
          />
          {option}
        </label>
      ))}
    </form>
  );
}

Note For checkboxes, read e.target.checked (boolean), not e.target.value. For selects, value goes on the <select> element, not on individual <option> elements. Radio buttons in the same group share the same name attribute.