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
| Item | Value |
|---|---|
| Handler location | <form onSubmit={handleSubmit}> |
| Prevent default | e.preventDefault() |
| Button type | type="submit" |
| Enter key | Triggers submit automatically |
| Handler can be async | Yes |
Submit Handler Pattern
| Step | Code |
|---|---|
| Prevent default | e.preventDefault() |
| Validate | const errors = validate() |
| Check errors | if (Object.keys(errors).length) return |
| Set submitting | setIsSubmitting(true) |
| Submit | await fetch(...) |
| Handle errors | try/catch or if (!response.ok) |
| Reset submitting | finally { setIsSubmitting(false) } |
Button States
| State | Disabled | Text |
|---|---|---|
| Idle | false | “Submit” |
| Invalid | true | “Submit” |
| Submitting | true | “Submitting…” |
Validation Timing
| When | Use |
|---|---|
| On submit | Final validation |
| On change | Live feedback |
| On blur | Field-level validation |
| Server response | Cross-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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Page reloads on submit | No preventDefault | Call e.preventDefault() |
| Handler not called | onSubmit on button | Move to <form> |
| Button does not submit | type="button" | Use type="submit" |
| Double submission | Button not disabled | Disable while submitting |
isSubmitting stuck true | No finally | Use finally to reset |
| Error not displayed | Not stored in state | setErrors and render |
| Form not reset | State not cleared | setForm(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
| Item | Value |
|---|---|
| Handler location | <form onSubmit={handleSubmit}> |
| Prevent default | e.preventDefault() |
| Submit button | type="submit" |
| Enter key | Triggers submission automatically |
| Async handler | async (e) => { e.preventDefault(); await ... } |
| Validation | Run in handler, set errors, return if invalid |
| Submitting state | isSubmitting boolean, reset in finally |
| Disabled button | disabled={isSubmitting} |
| Error display | Store in state, render conditionally |
| Form reset | setForm(initialState) |
Key takeaways:
- The
onSubmithandler 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 totype="submit"if no type is specified, but being explicit avoids confusion. Atype="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
isSubmittingstate prevents double submission. Set it totrueat the start of the submission andfalsein afinallyblock. Disable the button and inputs while it istrue. - The
finallyblock ensures the state is reset. Without it, an error in thetryblock leavesisSubmittingastrue, and the button stays disabled forever. Thefinallyblock 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!