Search code examples
javascriptreactjsformsform-submit

How do I fix this unwanted button behavior in ReactJS?


I'm trying to conditionally switch between two HTML buttons in ReactJS based on wether the form is shown or not. Basically if the form is shown, show a submit button and if not shown, show a regular button to show the form.

This is the code that I have. (I'm using Antd's button component but I also tried using the regular HTML button and had the same problem.

<div>
        {showForm ? (
          <Button
            loading={isUploading}
            htmlType="submit"
            form="videoForm"
            className={Styles.button}
          >
            {isUploading ? 'Uploading' : 'Upload Video'}
          </Button>
        ) : (
          <Button
            htmlType="button"
            onClick={() => setShowForm(true)}
            className={Styles.button}
          >
            {successMsg ? 'Upload Another Video' : 'Show Form'}
          </Button>
        )}
      </div>

The issue I'm having is that when the form is not shown and I click the Show Form button, the form shows correctly and the buttons switch, but the form submit event is triggering, which is not what I expected or want.

Any ideas why? And how I can fix this issue? I also tried doing the following but got the same results.

<div>
        <Button
          loading={isUploading}
          htmlType={showForm ? 'submit' : 'button'}
          form="videoForm"
          className={Styles.button}
        >
          {showForm && (isUploading ? 'Uploading' : 'Upload Video')}
          {!showForm && (successMsg ? 'Upload Another Video' : 'Show Form')}
        </Button>
      </div>

Any help would be greatly appreciated.


Solution

  • This behaviour is unrelated to the default type of the <button>, as OP is already setting the underlying HTML <button> element's type to 'button' via the htmlType prop. This behaviour can also be reproduced without React, as illustrated below:

    const form = document.getElementById('form'),
      btnWo = document.getElementById('btn-wo'),
      btnW = document.getElementById('btn-w');
    
    form.onsubmit = function (event) {
      console.log('form onsubmit');
      event.preventDefault();
    }
    
    btnWo.onclick = function (event) {
      console.log(`btn-wo onclick, type = ${this.type}`);
    }
    
    btnW.onclick = function (event) {
      console.log(`btn-w onclick, type = ${this.type}`);
      this.type = 'submit';
    }
    <form id="form">
      <button id="btn-wo" type="button">Button without Changing Type</button>
      <button id="btn-w" type="button">Button with Changing Type</button>
    </form>

    When "Button with Changing Type" is clicked, its type is changed to 'submit' in the event handler. After that event handler returns, the form is submitted because the clicked <button>'s type is now 'submit'.

    The same happens in OP's example because React doesn't remount the <Button> component (and the underlying HTML <button> element), only the htmlType prop (and the type property of the underlying element) is changed.

    To fix that, either:

    1. Call preventDefault() inside the event listener. This will prevent the default action of clicking a submit <button> associated with a form, which is submitting the form.

    const { useState } = React;
    
    const App = () => {
      const [showForm, setShowForm] = useState(false);
      const onSubmit = (event) => {
        console.log("form onSubmit");
        event.preventDefault();
        setShowForm(false);
      };
    
      return (
        <div>
          <div>
            {showForm ? (
              <button type="submit" form="videoForm">
                Upload Video
              </button>
            ) : (
              <button
                type="button"
                onClick={(event) => {
                  setShowForm(true);
                  event.preventDefault();
                }}
              >
                Show Form
              </button>
            )}
            {showForm && (
              <form id="videoForm" onSubmit={onSubmit}>
                <input />
              </form>
            )}
          </div>
        </div>
      );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
    <div id="root"></div>
    <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>

    1. Change the <button>'s type to 'submit' after a timeout. This works because the <button>'s type is still 'button' at the moment the event handler returns.

    const { useState } = React;
    
    const App = () => {
      const [showForm, setShowForm] = useState(false);
      const onSubmit = (event) => {
        console.log("form onSubmit");
        event.preventDefault();
        setShowForm(false);
      };
    
      return (
        <div>
          <div>
            {showForm ? (
              <button type="submit" form="videoForm">
                Upload Video
              </button>
            ) : (
              <button type="button" onClick={() => setTimeout(() => setShowForm(true))}>
                Show Form
              </button>
            )}
            {showForm && (
              <form id="videoForm" onSubmit={onSubmit}>
                <input />
              </form>
            )}
          </div>
        </div>
      );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
    <div id="root"></div>
    <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>

    1. Don't change the type of the <button>; use 1 <button> element for each type. In React, you can use the key prop on one of the components/elements to achieve this.

    const { useState } = React;
    
    const App = () => {
      const [showForm, setShowForm] = useState(false);
      const onSubmit = (event) => {
        console.log("form onSubmit");
        event.preventDefault();
        setShowForm(false);
      };
    
      return (
        <div>
          <div>
            {showForm ? (
              <button type="submit" form="videoForm">
                Upload Video
              </button>
            ) : (
              <button type="button" onClick={() => setShowForm(true)} key="s-btn">
                Show Form
              </button>
            )}
            {showForm && (
              <form id="videoForm" onSubmit={onSubmit}>
                <input />
              </form>
            )}
          </div>
        </div>
      );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
    <div id="root"></div>
    <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>