javascriptnode.jsreactjsreact-hooksreact-forms

React Form being submitted at the wrong time


I have two buttons, one of type "button" and one of type "submit", both wrapped in a form and which toggle each other. Weirdly, if I click on the button of type "button" the form is submitted and if I click of the button of type "submit" the form is not submitted.

const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);

function App() {
  const [clicked, setClicked] = React.useState(false);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        console.log("form submitted!");
      }}
    >
      {!clicked ? (
        <button type="button" onClick={() => setClicked(true)}>
          Button 1
        </button>
      ) : (
        <button type="submit" onClick={() => setClicked(false)}>
          Button 2
        </button>
      )}
    </form>
  );
}

root.render(
  <App />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>

I would expect that the opposite be true in regards to submitting the form.


Solution

  • Try this code, set manually the initial value of the showSubmitButton state to either true or false and you'll see that so far so good, the onSubmit event is looking for an input of type submit to fire and all works fine.
    you can also notice that the component rerenders before the onSubmit event handler runs.

    import { useState } from "react";
    
    const App = () => {
      const [counter, setCounter] = useState(0);
      const [showSubmitButton, setShowSubmitButton] = useState(true);
    
      return (
        <>
          {console.log("component rerender and counter is: ", counter)}
          <form
            onSubmit={(e) => {
              console.log(e);
              e.preventDefault();
              console.log("form submitted!");
            }}
          >
            {showSubmitButton ? (
              <button
                type="submit"
                onClick={(e) => {
                  console.log("submit button clicked");
                  setCounter((prev) => prev + 1);
                  // setShowSubmitButton((prev) => !prev);
                }}
              >
                Submit
              </button>
            ) : (
              <button
                type="button"
                onClick={() => {
                  console.log("simple button clicked");
                  setCounter((prev) => prev + 1);
                  // setShowSubmitButton((prev) => !prev);
                }}
              >
                Button
              </button>
            )}
          </form>
        </>
      );
    };
    export default App
    

    the drama begins when you uncomment setShowSubmitButton((prev) => !prev) in the submit button.
    now when you click it and toggle showSubmitButton, the component rerenders it is like the onSubmit event is triggered but cannot fire because the component rerenders and the input of type submit which is mandatory to do so cannot be found so nothing happens, till now, neither one of the two buttons is triggering onSubmit.

    now uncomment setShowSubmitButton((prev) => !prev) in the simple button.
    you'll see when you click that button the onSubmit event is firing and if you check e.target from inside onSubmit you will find it equal to

    <form>
      <button type="submit">Submit</button>
    </form>
    

    so when you click the submit button, it seems like the onSubmit event is stuck because the input of type submit cannot be found therefore when you click the simple button, the input of type submit is back to DOM, so the event handler can finally find it and run.
    I know this is crazy, but it is the only explanation, there is no way that the simple button is triggering onSubmit.


    if you move state updates inside the event handler after e.preventDefault():

     <>
          {console.log("component rerender and counter is: ", counter)}
          <form
            onSubmit={(e) => {
              console.log(e);
              e.preventDefault();
              console.log("form submitted!");
              setCounter((prev) => prev + 1);
              setShowSubmitButton((prev) => !prev);
            }}
          >
            {showSubmitButton ? (
              <button
                type="submit"
                onClick={(e) => {
                  console.log("submit button clicked");
                }}
              >
                Submit
              </button>
            ) : (
              <button
                type="button"
                onClick={() => {
                  console.log("simple button clicked");
                }}
              >
                Button
              </button>
            )}
          </form>
        </>
      );
    

    you will see it working as expected! because the component will rerender only when the code inside the onSubmit event handler function finishes