Search code examples
javascriptreactjsreact-hook-form

My submit button also submits another form


I'm using React, I have a form inside of another form:

When a user clicks on a specific button, a dialog shows up, which has the form that I want to be submitted.

My 2 forms have different IDs, but when I submit the form I want (which is in the dialog), the form in the background also submits.

I'm using react-hook-form to manage my forms, if this is also related.

I hope this image can give a little background on the situation: Submit button in my dialog becomes disabled when submitting the form, but the submit button in the background also becomes disabled because the other form is also submitting

I tried:

  • Using different IDs for my forms
  • Different onSubmit function names
  • Putting the onSubmit button in the <form> tag that I want to submit
  • Setting different unique IDs for my submit buttons.

Solution

  • TL; DR

    Use event.stopPropagation() outside React Hook Form handleSubmit:

    <form onSubmit={( event ) => {
      // Stop the event propagation OUTSIDE handleSubmit
      event.stopPropagation();
      // Explicitly pass the event
      handleSubmit(submitHandlerFunction)(event);
    }}>
      <button type="submit">Submit</button>
    </form>
    

    Explanations

    React Hook Form handleSubmit fires its submitHandler callback after asynchronous validation, so even if we try to use event.stopPropagation() inside our submitHandlerFunction, it is already too late to prevent the "parent" form from being submitted as well.

    You can see that the "parent" form is being submitted before our submitHandlerFunction runs:

    function App() {
      const { handleSubmit } = useForm();
    
      function submitHandlerFunction(data, event) {
        event.stopPropagation(); // Attempt stopping propagation, but it is already too late...
        console.log('submitHandlerFunction called');
      }
    
      return (
        <form
          onSubmit={(event) => {
            event.preventDefault();
            console.log('parent form submitted');
          }}
        >
          <h2>Parent form</h2>
          <Dialog>
            <form onSubmit={handleSubmit(submitHandlerFunction)}>
              <h3>Child react-hook-form</h3>
              <ol>
                <li>Calls parent onSubmit</li>
                <li>Calls child submitHandlerFunction</li>
              </ol>
              <button type="submit">Submit child form</button>
            </form>
          </Dialog>
        </form>
      );
    }
    

    Live demo: https://stackblitz.com/edit/vitejs-vite-66e51m?file=src%2FApp.jsx

    We have to call it outside handleSubmit:

            <form
              onSubmit={(event) => {
                // Stop the event propagation OUTSIDE handleSubmit
                event.stopPropagation();
                // Explicitly pass the event
                handleSubmit(submitHandlerFunction)(event);
              }}
            >
              <h3>Child react-hook-form stop propagation</h3>
              <ol>
                <li>Calls child submitHandlerFunction</li>
              </ol>
              <button type="submit">Submit child form</button>
            </form>
    

    We can also leverage some library to slightly simplify, e.g. browser-event-utils's withStopPropagation():

    Calls event.stopPropagation, then passes the event along to your provided event handler function.

            <form
              onSubmit={withStopPropagation(handleSubmit(submitHandlerFunction))}
            >
              <h3>Child react-hook-form withStopPropagation</h3>
              <ol>
                <li>Calls child submitHandlerFunction</li>
              </ol>
              <button type="submit">Submit child form</button>
            </form>
    

    More insights

    As mentioned by Code Spirit in the question comments, normally a <form> should not be nested inside another <form>, as per HTML spec:

    Note you are not allowed to nest FORM elements!

    See also Can you nest HTML forms?

    That being said, since your "child" <form> appears in a modal dialog, it probably is rendered in the DOM through a React Portal (e.g. if you use MUI Dialog). In that case, it is no longer nested inside the "parent" form, because the Portal "sends" its HTML content to another part of the DOM tree (in the case of MUI Dialog, as the last child of <body>).

    While we now technically comply with the HTML spec, unfortunately it will still not work in React, because of the behaviour of Portals regarding events propagation:

    Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. [...]

    This includes event bubbling. An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

    So we are back to square one: the "submit" action triggers the "child" and "parent" forms...


    There is also a possibility to associate form elements (here the submit button) to an explicit form, as also mentioned by Code Spirit in the question comments, by using a form="formId" attribute on them:

    A string specifying the <form> element with which the input is associated (that is, its form owner). This string's value, if present, must match the id of a <form> element in the same document. If this attribute isn't specified, the <input> element is associated with the nearest containing form, if any.

    The form attribute lets you place an input anywhere in the document but have it included with a form elsewhere in the document.

    But this helps when these elements are outside their <form> (i.e. not descendants in the DOM tree). It does not break the normal event propagation.


    As already implied, the solution is to stop the propagation of the "submit" event, so that the "parent" form does not receive it, and it does not fire its onSubmit callback.

    See also submit child form that was inside another form and prevent submitting parent form, react-hook-form

    But there is a subtlety with React Hook Form's handleSubmit, see above explanations.