Handling Form Submissions: onSubmit Event Handler
The onSubmit event handler intercepts form submissions in React, allowing you to process form data with JavaScript instead of allowing the browser's default behavior of reloading the page. By combining onSubmit with onChange and controlled component state, you can build fully interactive forms that respond instantly to user input and handle submission without a page refresh. This is essential for building modern single-page applications.
Key Takeaways
- The
onSubmithandler must be attached to the<form>element, not the submit button, to capture both button clicks and Enter key presses - Calling
event.preventDefault()insideonSubmitstops the browser from reloading the page, allowing JavaScript to handle submission - Controlled components use React state to hold form input values, making them accessible to the
onSubmithandler - Putting
onClickhandlers on submit buttons breaks keyboard accessibility and semantic correctness
The Submit Event: Capturing Form Submissions
In standard HTML, clicking a <button type="submit"> inside a <form> triggers the browser's default submit behavior: the form data is serialized and sent to the server (specified by the form's action attribute), and the page reloads. In a React single-page application (SPA), this default behavior prevents you from handling submissions with JavaScript and updating the UI without a page refresh.
The onSubmit event handler, attached to the <form> element, intercepts this event. Inside your handler, event.preventDefault() stops the default reload behavior, giving your JavaScript code a chance to process the form data, validate it, send it to an API, and update the UI.
When onSubmit Fires
The form's onSubmit event fires in two scenarios:
- A user clicks a
<button type="submit">inside the form - A user presses Enter while focus is on a text input field inside the form
Both cases trigger onSubmit on the <form> element, ensuring your handler is called regardless of how the user initiates submission.
Building a Complete Controlled Form
A controlled form in React stores input values in component state via useState. Each input's onChange handler updates the corresponding state variable. When the form is submitted, your onSubmit handler has immediate access to the latest form values through state.
import React, { useState } from 'react';
export default function LoginForm() {
// 1. State for each input
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
// 2. The submission handler
function handleSubmit(event) {
// 3. Prevent the default form submission behavior
event.preventDefault();
// 4. Validate the form data
if (!username || !password) {
setError('Both fields are required');
return;
}
// 5. Handle the form data (in production, send to API)
console.log('Submitting:', { username, password });
setError('');
alert(`Logged in as: ${username}`);
// Clear the form after successful submission
setUsername('');
setPassword('');
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Enter your username"
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Enter your password"
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Log In</button>
</form>
);
}
Code Breakdown:
-
State Variables:
usernameandpasswordstate variables hold the current form input values. These are the single source of truth for what's displayed in the inputs. -
onChange Handlers: Each input's
onChangehandler updates its corresponding state variable withe.target.value, keeping the state in sync with the input. -
handleSubmit Function: This function is called when the form is submitted. The
eventparameter is aSyntheticEventobject representing the form submission. -
event.preventDefault(): This method stops the browser from performing its default submit behavior (serializing data and reloading the page). Without this, the page would refresh and state would be lost.
-
Validation: After preventing the default behavior, validate the form data. If validation fails, update error state and return early.
-
Processing: Once validation passes, process the data: log it, send it to an API, or update local state as needed.
-
Form Attachment: The
onSubmit={handleSubmit}prop is attached to the<form>element, not the button. -
Submit Button: A
<button type="submit">inside the form automatically triggers the form'sonSubmitevent when clicked or when Enter is pressed in a text field.
Handling Multiple Input Types
Forms often contain multiple input types. The controlled form pattern works with all of them:
import React, { useState } from 'react';
export default function SignupForm() {
const [formData, setFormData] = useState({
email: '',
age: '',
subscribe: false,
country: 'US'
});
function handleChange(e) {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
}
function handleSubmit(e) {
e.preventDefault();
console.log('Form submitted:', formData);
// Send to API or update state
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</label>
<label>
Age:
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
/>
</label>
<label>
Country:
<select name="country" value={formData.country} onChange={handleChange}>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="CA">Canada</option>
</select>
</label>
<label>
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
<button type="submit">Sign Up</button>
</form>
);
}
This pattern scales to any number of inputs. A single handleChange function updates the appropriate state field based on the input's name attribute.
onSubmit vs. onClick: The Critical Difference
A common mistake is to put submission logic on the submit button's onClick handler instead of the form's onSubmit:
// ANTI-PATTERN: Don't do this
<form>
<input type="text" value={username} onChange={e => setUsername(e.target.value)} />
<button type="submit" onClick={handleSubmit}>Log In</button>
</form>
Why This Pattern Is Wrong
-
Breaks Keyboard Accessibility: Users expect to submit a form by pressing Enter in an input field. The
onClickhandler only fires on button clicks, breaking this essential accessibility feature. Keyboard-only users cannot submit the form. -
Semantically Incorrect: The submit event belongs to the form as a whole, not just the button. Using
onSubmitcorrectly expresses the component's intent. -
Inconsistent Behavior: Different browsers and devices may trigger events differently.
onSubmiton the form handles all cases consistently.
The Correct Pattern
// CORRECT: Use onSubmit on the form element
<form onSubmit={handleSubmit}>
<input type="text" value={username} onChange={e => setUsername(e.target.value)} />
<button type="submit">Log In</button>
</form>
Always attach submission handlers to the <form> element's onSubmit prop. This ensures your form works with keyboard navigation and follows React and web standards.
Advanced Patterns: Async Submission and Loading States
In production, form submissions often involve asynchronous operations like API calls. You can manage this with additional state:
import React, { useState } from 'react';
export default function SubmitForm() {
const [formData, setFormData] = useState({ email: '' });
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');
function handleChange(e) {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
}
async function handleSubmit(e) {
e.preventDefault();
setIsLoading(true);
setMessage('');
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) throw new Error('Subscription failed');
setMessage('Subscribed successfully!');
setFormData({ email: '' });
} catch (error) {
setMessage(`Error: ${error.message}`);
} finally {
setIsLoading(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="[email protected]"
required
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Subscribing...' : 'Subscribe'}
</button>
{message && <p>{message}</p>}
</form>
);
}
This pattern demonstrates:
- Disabling inputs and button while the request is in flight
- Showing loading feedback to the user
- Handling success and error cases
- Clearing the form after successful submission
Frequently Asked Questions
Why must I call preventDefault in onSubmit?
Without preventDefault(), the browser executes its default form submission behavior: serializing the form's data and sending it to the server (or reloading the page if no action is specified). This causes a full page reload, destroying your component's state and the single-page application experience. preventDefault() stops this, allowing JavaScript to handle the submission instead.
What happens if a user presses Enter in a text input?
If the form contains a single text input, pressing Enter submits the form and fires onSubmit. If the form contains multiple inputs (like a multi-step form), pressing Enter still submits the entire form. This is standard HTML form behavior. To submit on a different trigger or handle multi-step forms, manage state to show/hide form sections or use separate forms.
Should I use a single onChange handler or separate handlers for each input?
Both approaches work. A single handleChange function with a name attribute on each input scales better for large forms and reduces code duplication. Separate handlers are fine for 2-3 inputs but become verbose in larger forms. For the best readability and maintainability, use a single handler with structured state.
Can I submit a form without a submit button?
Yes. You can call a function that triggers submission logic directly. However, if you want semantic HTML and keyboard accessibility (Enter key submission), use a <form> with an onSubmit handler and a submit button. For custom submission patterns, explicitly handle keyboard and click events on the container element.
How do I validate form inputs before submission?
Perform validation inside your onSubmit handler after calling preventDefault(). Check field values against your requirements (empty, format, length, etc.). If validation fails, display error messages in state but do not proceed to submission. If validation passes, proceed with API calls or state updates. You can also use HTML5 validation attributes like required, pattern, and type="email", but JavaScript validation gives you more control.
Glossary
onSubmit: A React event handler attached to a <form> element that fires when the form is submitted, either by clicking a button with type="submit" or by pressing Enter in a text input.
event.preventDefault(): A method on the event object that stops the browser from performing its default action for that event. For form submissions, this prevents the page from reloading and allows JavaScript to handle the submission.
Controlled Component: A form input whose value is managed by React state via the value prop and onChange handler, making the component the single source of truth for input values.
SyntheticEvent: React's cross-browser wrapper around the browser's native event object, providing a consistent API for event handling across all browsers.
type="submit": An HTML button attribute that designates a button as a form submit button. Clicking it or pressing Enter in a form input triggers the form's onSubmit event.