← Back to React Course | Chapter 7: Forms & User Input | Lesson 7 of 8

Form Submission and Loading States

Handling form submission with loading states is like showing a 'please wait' sign while your order is being cooked, so you know it's actually being processed.

Tracking Submission State

A boolean state like submitting tracks whether the form is currently being processed. Setting it to true right when submission starts, and back to false once it finishes, lets the UI show a loading indicator and disable the button during that window.

Note: Name it isSubmitting or submitting so its purpose is obvious at a glance in JSX conditions.

Warning: If you forget to set submitting back to false after the request finishes, the button stays disabled forever.

Example: Tracking Submission State

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [submitting, setSubmitting] = React.useState(false);
  const handleSubmit = e => {
    e.preventDefault();
    setSubmitting(true);
    setTimeout(() => setSubmitting(false), 1000);
  };
  return <form onSubmit={handleSubmit}><button disabled={submitting}>{submitting ? "Submitting..." : "Submit"}</button></form>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Handling Success and Error Outcomes

A real submission can succeed or fail, so it's useful to track a result state (like idle, success, or error) in addition to the loading flag. This lets the UI show the right message once the request finishes, instead of just silently resetting.

Note: Reset the result state back to idle when the user starts editing the form again after a previous error.

Warning: Showing a stale success message after the user has already changed the form again can be confusing — clear it on new input.

Example: Handling Success and Error Outcomes

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [status, setStatus] = React.useState("idle");
  const handleSubmit = e => {
    e.preventDefault();
    setStatus("submitting");
    setTimeout(() => setStatus("success"), 800);
  };
  return (
    <form onSubmit={handleSubmit}>
      <button disabled={status === "submitting"}>Submit</button>
      {status === "success" && <p>Saved successfully!</p>}
    </form>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Using try/catch/finally for Reliable Cleanup

Wrapping the async submission logic in try/catch/finally guarantees the loading state resets no matter what happens — whether the request succeeds, throws an error, or the network fails. The finally block always runs, making it the safest place to turn off the loading flag.

Note: Put setSubmitting(false) in finally, not duplicated in both the try and catch blocks, to avoid repeating yourself.

Warning: Without finally, an error thrown before you reach the success setSubmitting(false) line leaves the button permanently disabled.

Example: Using try/catch/finally for Reliable Cleanup

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState("");
  const handleSubmit = async e => {
    e.preventDefault();
    setSubmitting(true);
    try { throw new Error("Simulated failure"); }
    catch (err) { setError(err.message); }
    finally { setSubmitting(false); }
  };
  return <form onSubmit={handleSubmit}><button disabled={submitting}>Submit</button>{error && <p>{error}</p>}</form>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Not disabling the submit button while a submission is in progress, allowing duplicate submits.
  2. Forgetting to reset the loading state in a catch block, leaving the button stuck disabled forever after an error.
  3. Not showing any success or error feedback after the submission finishes.
Chapter Summary
  • A submitting state (boolean) tracks whether a form request is currently in flight.
  • The submit button should be disabled while submitting to prevent duplicate submissions.
  • try/catch/finally ensures the loading state resets whether the submission succeeds or fails.
  • Showing distinct success and error messages gives the user clear feedback on the outcome.
Browser Support

No React-version restriction — built on standard async/await and useState.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.