| |

React 28 ⚛️ Form Submission and Preventing Default Behavior

A form has a default behavior. When the user presses Enter in a text field, or clicks a submit button, the browser submits the form. In a traditional server-rendered application, this is the desired behavior: the browser sends the form data to the server, and the server responds with a new page. In a React application, it is almost never what you want. The page reloads, the component state is lost, and the single-page application is no longer single-page. Preventing the default submission and handling the data in JavaScript is the standard pattern, and it starts with event.preventDefault().

This chapter covers form submission in React. You will learn how to attach an onSubmit handler to the <form> element, how to prevent the default behavior, how to read the form data, how to handle validation and async submission, and how to manage the submitting state. You will also see the differences between controlled and uncontrolled submission, and the patterns that keep the submit logic clean.

Key point: The onSubmit handler goes on the <form> element, not on the submit button. The handler receives the submit event and calls event.preventDefault() to stop the browser from reloading the page. The form data is read from the component’s state (controlled) or from the form elements (uncontrolled). The submit button must have type="submit" to trigger the form’s onSubmit.


Why form submission needs handling

The page reload problem. The default behavior of a form submission is to navigate to a new URL. In a React single-page application, this reloads the entire page, discarding the component tree, the state, and any client-side routing. The user sees a flash, loses their place, and the application restarts. event.preventDefault() stops the navigation and lets the JavaScript handle the submission.

The data problem. When the browser submits a form natively, the data is sent as form-encoded key-value pairs. In a React application, the form data usually lives in component state, and it needs to be sent as JSON, or transformed, or validated before submission. Handling the submission in JavaScript gives full control over the data format and the transport.

The validation problem. Client-side validation runs before submission. If the form is invalid, the submission should be blocked, and the errors should be displayed. The onSubmit handler is the place to run the validation and decide whether to proceed.

The async problem. Submitting a form often involves an asynchronous request—a fetch call to an API. The handler must await the response, handle errors, and update the UI. The default form submission is synchronous and navigates away before any of this can happen.

The user feedback problem. During submission, the user should see that something is happening. The submit button should be disabled, a spinner should appear, and the form should not be submitted twice. Managing this state is part of the submit handler.


a. The onSubmit handler and preventDefault

The onSubmit handler is attached to the <form> element. It is called when the form is submitted, either by clicking a submit button or by pressing Enter in a field.

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Submitting:', { email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Log in</button>
    </form>
  );
}

The onSubmit handler receives the submit event. The first line calls e.preventDefault() to stop the browser from reloading the page. The rest of the handler reads the state and does something with it.

The submit button has type="submit". This is what triggers the form’s onSubmit when clicked. A button without a type inside a form defaults to type="submit" in HTML, but being explicit is clearer. A button with type="button" does not submit the form; it is a regular button.

Pressing Enter in a text input also triggers the form’s onSubmit. This is the default browser behavior for forms with a single text input or with a submit button. The handler does not need to do anything special to support Enter; it works automatically.

The handler can be async:

const handleSubmit = async (e) => {
  e.preventDefault();
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  const data = await response.json();
  console.log(data);
};

The preventDefault is called first, before any await. This is important: the event object is pooled in React’s synthetic event system (though this is less of an issue in React 17+), and calling preventDefault synchronously ensures it is applied before the handler yields.


b. Validation and error handling

Validation runs in the submit handler before the data is sent. If the form is invalid, the handler returns early and displays the errors.

function SignupForm() {
  const [form, setForm] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};
    if (!form.email.includes('@')) {
      newErrors.email = 'Invalid email';
    }
    if (form.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const newErrors = validate();
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }
    setErrors({});
    // proceed with submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={form.email}
        onChange={(e) => setForm({ ...form, email: e.target.value })}
      />
      {errors.email && <p className="error">{errors.email}</p>}
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={(e) => setForm({ ...form, password: e.target.value })}
      />
      {errors.password && <p className="error">{errors.password}</p>}
      <button type="submit">Sign up</button>
    </form>
  );
}

The validate function returns an object of errors. If the object has any keys, the form is invalid, and the handler sets the errors and returns. If it is empty, the errors are cleared and the submission proceeds.

Errors can also be handled from the server response. A server might return a 400 with a validation error, or a 401 with an authentication error. The handler checks the response and sets the appropriate error state:

const handleSubmit = async (e) => {
  e.preventDefault();
  try {
    const response = await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    });
    if (!response.ok) {
      const error = await response.json();
      setErrors({ server: error.message });
      return;
    }
    // success
  } catch (err) {
    setErrors({ server: 'Network error' });
  }
};

The try/catch handles network errors. The response.ok check handles HTTP errors. The error message is stored in state and displayed.


c. Submitting state and disabling the button

During an async submission, the user should not be able to submit again. The submit button should be disabled, and a loading indicator should appear.

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} disabled={isSubmitting} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} disabled={isSubmitting} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in…' : 'Log in'}
      </button>
    </form>
  );
}

The isSubmitting state is set to true at the start of the submission and false at the end, in a finally block that runs whether the request succeeds or fails. The button is disabled while submitting, and its text changes to indicate progress.

The finally block is important. Without it, an error in the try block would leave isSubmitting as true, and the button would remain disabled forever. The finally block ensures the state is reset regardless of the outcome.

The disable logic can be combined with validation. The button can be disabled when the form is invalid, in addition to when it is submitting:

const isValid = email.includes('@') && password.length >= 8;
<button type="submit" disabled={!isValid || isSubmitting}>
  {isSubmitting ? 'Logging in…' : 'Log in'}
</button>

The button is disabled when the form is invalid or when it is submitting. The user cannot submit an invalid form or double-submit a valid one.


Complete Example Session

// ============================================
// PART 1: BASIC FORM SUBMISSION
// ============================================
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Submitting:', { email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}
// ============================================
// PART 2: SUBMIT ON ENTER
// ============================================
// Pressing Enter in a text input triggers form submission
// when the form has a submit button.
// No extra code is needed.
// ============================================
// PART 3: ASYNC SUBMISSION
// ============================================
const handleSubmit = async (e) => {
  e.preventDefault();
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  const data = await response.json();
  console.log(data);
};
// ============================================
// PART 4: CLIENT VALIDATION
// ============================================
const validate = () => {
  const errors = {};
  if (!email.includes('@')) errors.email = 'Invalid email';
  if (password.length < 8) errors.password = 'Too short';
  return errors;
};

const handleSubmit = (e) => {
  e.preventDefault();
  const errors = validate();
  if (Object.keys(errors).length > 0) {
    setErrors(errors);
    return;
  }
  // proceed
};
// ============================================
// PART 5: DISPLAYING VALIDATION ERRORS
// ============================================
{errors.email && <p className="error">{errors.email}</p>}
{errors.password && <p className="error">{errors.password}</p>}
// ============================================
// PART 6: SERVER ERROR HANDLING
// ============================================
const handleSubmit = async (e) => {
  e.preventDefault();
  try {
    const response = await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    });
    if (!response.ok) {
      const error = await response.json();
      setErrors({ server: error.message });
      return;
    }
  } catch (err) {
    setErrors({ server: 'Network error' });
  }
};
// ============================================
// PART 7: SUBMITTING STATE
// ============================================
const [isSubmitting, setIsSubmitting] = useState(false);

const handleSubmit = async (e) => {
  e.preventDefault();
  setIsSubmitting(true);
  try {
    await fetch(/* ... */);
  } finally {
    setIsSubmitting(false);
  }
};
// ============================================
// PART 8: DISABLED SUBMIT BUTTON
// ============================================
<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? 'Submitting…' : 'Submit'}
</button>
// ============================================
// PART 9: DISABLED INPUTS DURING SUBMISSION
// ============================================
<input value={email} onChange={...} disabled={isSubmitting} />
<input type="password" value={password} onChange={...} disabled={isSubmitting} />
// ============================================
// PART 10: FORM RESET AFTER SUBMISSION
// ============================================
const handleSubmit = async (e) => {
  e.preventDefault();
  setIsSubmitting(true);
  try {
    await fetch(/* ... */);
    setForm({ email: '', password: '' });  // reset
  } finally {
    setIsSubmitting(false);
  }
};

The ten parts covered basic submission, submit on Enter, async submission, client validation, error display, server error handling, submitting state, disabled button, disabled inputs, and form reset.


Quick Reference

Form Submission Essentials

ItemValue
Handler location<form onSubmit={handleSubmit}>
Prevent defaulte.preventDefault()
Button typetype="submit"
Enter keyTriggers submit automatically
Handler can be asyncYes

Submit Handler Pattern

StepCode
Prevent defaulte.preventDefault()
Validateconst errors = validate()
Check errorsif (Object.keys(errors).length) return
Set submittingsetIsSubmitting(true)
Submitawait fetch(...)
Handle errorstry/catch or if (!response.ok)
Reset submittingfinally { setIsSubmitting(false) }

Button States

StateDisabledText
Idlefalse“Submit”
Invalidtrue“Submit”
Submittingtrue“Submitting…”

Validation Timing

WhenUse
On submitFinal validation
On changeLive feedback
On blurField-level validation
Server responseCross-field or business rules

Best Practices

✅ Do This:

// Attach onSubmit to the form
<form onSubmit={handleSubmit}>                                    // ✅

// Call preventDefault first
const handleSubmit = (e) => {
  e.preventDefault();
  // ...
};                                                                // ✅

// Use type="submit" on the submit button
<button type="submit">Submit</button>                             // ✅

// Disable the button while submitting
<button disabled={isSubmitting}>                                  // ✅

// Reset isSubmitting in finally
try { await submit(); } finally { setIsSubmitting(false); }        // ✅

// Show error messages from the server
if (!response.ok) setErrors({ server: 'Failed' });                // ✅

// Reset the form after successful submission
setForm(initialState);                                            // ✅

❌ Don’t Do This:

// Don't attach onSubmit to the button
<button onClick={handleSubmit}>Submit</button>                    // ⚠️

// Don't forget preventDefault
const handleSubmit = (e) => {
  // page reloads                                                 // ❌
};

// Don't use type="button" for the submit button
<button type="button">Submit</button>                             // ❌

// Don't leave isSubmitting true after an error
setIsSubmitting(true);
await submit();
setIsSubmitting(false);  // skipped on error                      // ❌

// Don't allow double submission
<button>Submit</button>  // not disabled                          // ⚠️

// Don't ignore the response status
await fetch(...);  // no response.ok check                        // ❌

Common Pitfalls

PitfallWhy It HappensFix
Page reloads on submitNo preventDefaultCall e.preventDefault()
Handler not calledonSubmit on buttonMove to <form>
Button does not submittype="button"Use type="submit"
Double submissionButton not disabledDisable while submitting
isSubmitting stuck trueNo finallyUse finally to reset
Error not displayedNot stored in statesetErrors and render
Form not resetState not clearedsetForm(initialState)

Real-World Examples

1. Login Form

<form onSubmit={handleSubmit}>
  <input value={email} onChange={(e) => setEmail(e.target.value)} />
  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
  <button type="submit">Log in</button>
</form>

2. Signup with Validation

const handleSubmit = (e) => {
  e.preventDefault();
  const errors = validate();
  if (Object.keys(errors).length) return setErrors(errors);
  // proceed
};

3. Async Submission with Loading

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? 'Saving…' : 'Save'}
</button>

4. Server Error Display

{errors.server && <p className="error">{errors.server}</p>}

5. Form Reset

const reset = () => setForm({ email: '', password: '' });

6. Disabled Inputs While Submitting

<input disabled={isSubmitting} value={email} onChange={...} />

7. Submit on Enter

// Pressing Enter in a text input triggers form submit
// No extra code needed

8. Prevent Double Submit

<button type="submit" disabled={isSubmitting}>Submit</button>

9. Validation Before Submit

const isValid = email.includes('@') && password.length >= 8;
<button type="submit" disabled={!isValid || isSubmitting}>Submit</button>

10. Success Message

const [success, setSuccess] = useState(false);
// after submit: setSuccess(true);
{success && <p>Form submitted successfully.</p>}

Visual

Form Submission Flow

┌─────────────────────────────────────────────────────────────┐
│  FORM SUBMISSION FLOW                                       │
│                                                             │
│  User clicks Submit or presses Enter                        │
│    │                                                        │
│    ▼                                                        │
│  Browser fires submit event                                 │
│    │                                                        │
│    ▼                                                        │
│  onSubmit handler runs                                      │
│    │                                                        │
│    ▼                                                        │
│  e.preventDefault()                                         │
│    │                                                        │
│    ├── Without it: page reloads, state lost                 │
│    └── With it: JavaScript handles submission               │
│    │                                                        │
│    ▼                                                        │
│  Validate form                                              │
│    │                                                        │
│    ├── Invalid ──▶ setErrors, return                       │
│    └── Valid ──▶ continue                                  │
│    │                                                        │
│    ▼                                                        │
│  setIsSubmitting(true)                                      │
│    │                                                        │
│    ▼                                                        │
│  await fetch(...)                                           │
│    │                                                        │
│    ▼                                                        │
│  Handle response                                            │
│    │                                                        │
│    ▼                                                        │
│  finally: setIsSubmitting(false)                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Submit Button Types

┌─────────────────────────────────────────────────────────────┐
│  type="submit"                                              │
│                                                             │
│  Triggers form onSubmit when clicked.                       │
│  <button type="submit">Submit</button>                      │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  type="button"                                              │
│                                                             │
│  Does not submit the form.                                  │
│  Used for other actions (cancel, reset, add row).           │
│  <button type="button">Cancel</button>                      │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  no type (default)                                          │
│                                                             │
│  Inside a form, defaults to type="submit".                  │
│  Be explicit for clarity.                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Submitting State

┌─────────────────────────────────────────────────────────────┐
│  IDLE                                                       │
│                                                             │
│  isSubmitting = false                                       │
│  Button: enabled, "Submit"                                  │
│  Inputs: enabled                                            │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  SUBMITTING                                                 │
│                                                             │
│  isSubmitting = true                                        │
│  Button: disabled, "Submitting…"                            │
│  Inputs: disabled                                           │
│                                                             │
│  Prevents double submission.                                │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  COMPLETE                                                   │
│                                                             │
│  isSubmitting = false                                       │
│  Button: enabled, "Submit"                                  │
│  Inputs: enabled                                            │
│  Form: reset or success message shown                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Error Handling Layers

┌─────────────────────────────────────────────────────────────┐
│  CLIENT VALIDATION                                          │
│                                                             │
│  Runs before the request.                                   │
│  Checks field format, required fields, lengths.             │
│  Errors displayed inline next to fields.                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  NETWORK ERROR                                              │
│                                                             │
│  Catches fetch failures.                                    │
│  try/catch around the await.                                │
│  Displayed as a general error message.                      │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  HTTP ERROR                                                 │
│                                                             │
│  response.ok is false.                                      │
│  Parse the error body, display the message.                 │
│  May include field-specific errors from the server.         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Handler location<form onSubmit={handleSubmit}>
Prevent defaulte.preventDefault()
Submit buttontype="submit"
Enter keyTriggers submission automatically
Async handlerasync (e) => { e.preventDefault(); await ... }
ValidationRun in handler, set errors, return if invalid
Submitting stateisSubmitting boolean, reset in finally
Disabled buttondisabled={isSubmitting}
Error displayStore in state, render conditionally
Form resetsetForm(initialState)

Key takeaways:

  • The onSubmit handler goes on the <form>, not the button. The form element receives the submit event, whether it is triggered by a button click or by pressing Enter in a field. Attaching the handler to the button misses the Enter key and other submission paths.
  • e.preventDefault() stops the browser from reloading the page. Without it, the form submits natively, the page navigates, and the React application loses its state. This is the first line of every submit handler.
  • The submit button must have type="submit". A button inside a form defaults to type="submit" if no type is specified, but being explicit avoids confusion. A type="button" button does not submit the form.
  • Pressing Enter in a text input triggers the form’s onSubmit. This is default browser behavior and requires no extra code. It is the reason the handler goes on the form rather than on the button.
  • Validation runs in the submit handler. Client-side validation checks the form before the request is sent. If the form is invalid, the handler sets the errors and returns without submitting.
  • The isSubmitting state prevents double submission. Set it to true at the start of the submission and false in a finally block. Disable the button and inputs while it is true.
  • The finally block ensures the state is reset. Without it, an error in the try block leaves isSubmitting as true, and the button stays disabled forever. The finally block runs whether the request succeeds or fails.
  • Server errors are handled separately from client errors. Client validation catches format issues before the request. Server errors—authentication failures, business rule violations, network problems—are handled after the response and displayed as general or field-specific messages.

Remember: Form submission in React is JavaScript, not the browser’s default. The onSubmit handler on the form, the preventDefault() call, and the type="submit" button are the three pieces that make it work. Validation, submitting state, error handling, and form reset are the layers that make it robust. The pattern is the same whether the form has two fields or twenty: prevent the default, validate, set the submitting state, make the request, handle the response, and reset. Master this pattern, and every form in every React application will behave predictably.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!