In a React component, why does submitting a <form> reload the page, and what must the onSubmit handler do to stop that?
answer
- a form submit is a navigation
- no action attribute still submits
- first line of the handler
- handler on the form, not the button
- every non-submit button needs a type
basics
~20 sThe browser's default form submission navigates, which reloads the page and throws away React state. Call event.preventDefault() as the first thing in the form's onSubmit handler, then read your state and send the request yourself.
solid answer
~50 sSubmitting a form is a browser navigation: with no `action` attribute it re-requests the current URL, the page reloads, and every piece of React state is gone. React does not suppress that for you when you use `onSubmit` — the handler runs, then the default proceeds. So the first line of the handler is `event.preventDefault()`, and everything after it is yours: read the values out of state, run validation, call the API, set a pending flag so the button can be disabled against double submits. I put the handler on the `<form>` and not on the button's `onClick`, because `onSubmit` catches every path into a submission — Enter in a text field, a click on the submit button, a programmatic `requestSubmit()` — and it is the one place `preventDefault` belongs. In React 19 there is also an `action` prop that takes a function, and React handles the default itself in that case.
code
javascript · 35 linesimport { useState } from 'react';
export default function ContactForm() {
const [message, setMessage] = useState('');
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
setPending(true);
setError(null);
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
if (!res.ok) throw new Error('Request failed');
setMessage('');
} catch (err) {
setError('Could not send. Please try again.');
} finally {
setPending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
{error && <p>{error}</p>}
<button type="submit" disabled={pending}>{pending ? 'Sending…' : 'Send'}</button>
<button type="button" onClick={() => setMessage('')}>Clear</button>
</form>
);
}go deeper
Be able to write the handler on the spot: event.preventDefault() first, then read state and send. Say plainly that a form submit is a browser navigation that would wipe React state.
Explain why the handler belongs on the form rather than the button — Enter, clicks and requestSubmit all funnel through onSubmit — and why an untyped button inside a form submits it.
Cover the full submit lifecycle you would ship: whole-form validation, a pending flag cleared in a finally, an error path that preserves the user's input, and protection against duplicate submissions.
Own the pattern across the product: one submit shape every form follows, a decision on whether new forms use the onSubmit path or React 19's action prop, and the rule that server-side validation is mandatory regardless of what the client does.
## What the default actually is A `<form>` is a navigation element. Submitting it makes the browser build a request from the form's fields and navigate to the URL in its `action` attribute using its `method`. If there is no `action`, the target is the current URL. Either way the document unloads and a fresh one is loaded, which for a single-page React app means the component tree is destroyed and rebuilt, every `useState` value is back to its initial value, and any half-finished work is gone. What the user perceives is "the page flashed and my form is empty". This is HTML behaviour, not React behaviour. It predates you and it is the default because forms worked this way before JavaScript existed. ## Preventing it ```jsx function handleSubmit(event) { event.preventDefault(); // now it's an ordinary function call if (!isValid(values)) return; submitToApi(values); } return <form onSubmit={handleSubmit}>…</form>; ``` `event.preventDefault()` cancels the browser's default action for that event. React does not do this implicitly for `onSubmit`; your handler runs and then the navigation happens unless you stop it. The event object React hands you is a SyntheticEvent, and `preventDefault` on it does what you expect on the underlying native event. Call it first, unconditionally. A handler that only prevents the default on the validation-failure path will still navigate away on the success path, which is a bug that hides behind "well, it worked when the data was wrong". ## Put the handler on the form A common alternative is `onClick` on the submit button. It mostly works, and that is what makes it dangerous. There are several ways to submit a form: clicking a submit button, pressing Enter in a single-line text field ("implicit submission", which browsers usually route through a synthetic click on the default submit button — but not when the form has no submit button), and calling `form.requestSubmit()` from code. `onSubmit` on the form is the single funnel every one of these passes through. It is also where `preventDefault` semantically belongs: you are cancelling a *submission*, not a *click*. A related trap lives in the same neighbourhood: a `<button>` inside a form with no `type` attribute defaults to `type="submit"`. So a button whose `onClick` opens a modal or adds a row will *also* submit and reload the form. Any button in a form that is not the submit button needs `type="button"` explicitly. ## After preventDefault Once the default is cancelled, the submit path is ordinary React code: - validate the whole values object, not only the fields the user touched, and bail out early if it fails; - set a pending flag so you can disable the submit button and render a spinner — otherwise an impatient user fires the request two or three times; - `await` the request and handle both outcomes: on failure, surface a form-level error and keep the values so the user does not retype them; on success, reset or navigate; - clear the pending flag in both branches, typically in a `finally`, or the button stays disabled forever after an error. ```jsx async function handleSubmit(event) { event.preventDefault(); setPending(true); try { await submitToApi(values); } catch (err) { setFormError('Could not save. Please try again.'); } finally { setPending(false); } } ``` ## Do not forget the server Cancelling the default submission means the request now leaves on your terms — your URL, your headers, your JSON body. It does not mean the data has been checked. Whatever the client validated, the server validates again. ## The React 19 alternative React 19 also lets you pass a *function* to a form's `action` prop rather than a URL. In that mode React calls your function when the form is submitted and takes care of the default navigation itself, so there is no `preventDefault` to write. It is a different submission model with its own pending and result plumbing; the `onSubmit` + `preventDefault` pattern described here is still completely valid, and it is what you will find in most existing code.
- Why put the handler on the form's onSubmit rather than on the submit button's onClick?Because onSubmit is the funnel every submission path goes through: a click on the submit button, Enter pressed in a single-line text input, and a programmatic form.requestSubmit(). Enter usually does route through a click on the default submit button, but a form with no submit button, or a scripted submission, bypasses onClick entirely. preventDefault also belongs on the submission, not on a click that happens to cause one.
- A button inside the form only opens a modal, yet clicking it reloads the page. What is wrong?The button has no type attribute, and the HTML default for a button inside a form is type="submit". So its onClick runs, then the form submits and the browser navigates. Give every button in a form that is not the submit button an explicit type="button". This is the single most common accidental-submit bug in React forms.
- How do you stop an impatient user from submitting the same form three times?Keep a pending flag in state, set it right after preventDefault, and disable the submit button while it is true, clearing it in a finally so a failed request does not leave the button dead. That covers the UI side; because a determined client can still send duplicates, anything that must happen exactly once also needs an idempotency guard on the server.
saying these in an interview costs you the question
- Thinks React cancels the default submission for you
- Says a form without an action attribute does not submit
- Calls preventDefault only on the validation-failure path
- Uses onClick on the button as the submit handler
- Leaves other buttons in the form without type="button"
- Never disables the button while the request is in flight